2019-07-21 20:01:51 +00:00
|
|
|
package format
|
2019-06-07 19:35:23 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
2020-11-10 18:57:09 +00:00
|
|
|
"fmt"
|
|
|
|
"regexp"
|
2019-06-07 19:35:23 +00:00
|
|
|
"strings"
|
2020-07-26 23:48:22 +00:00
|
|
|
"time"
|
2019-06-07 19:35:23 +00:00
|
|
|
"unicode"
|
|
|
|
|
2021-11-05 09:19:46 +00:00
|
|
|
"git.sr.ht/~rjarry/aerc/models"
|
2020-11-10 18:57:09 +00:00
|
|
|
"github.com/emersion/go-message/mail"
|
2019-06-07 19:35:23 +00:00
|
|
|
)
|
|
|
|
|
2020-11-10 18:57:09 +00:00
|
|
|
// AddressForHumans formats the address. If the address's name
|
|
|
|
// contains non-ASCII characters it will be quoted but not encoded.
|
|
|
|
// Meant for display purposes to the humans, not for sending over the wire.
|
|
|
|
func AddressForHumans(a *mail.Address) string {
|
|
|
|
if a.Name != "" {
|
|
|
|
if atom.MatchString(a.Name) {
|
|
|
|
return fmt.Sprintf("%s <%s>", a.Name, a.Address)
|
|
|
|
} else {
|
|
|
|
return fmt.Sprintf("\"%s\" <%s>",
|
|
|
|
strings.ReplaceAll(a.Name, "\"", "'"), a.Address)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return fmt.Sprintf("<%s>", a.Address)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var atom *regexp.Regexp = regexp.MustCompile("^[a-z0-9!#$%7'*+-/=?^_`{}|~ ]+$")
|
|
|
|
|
2020-11-10 19:27:30 +00:00
|
|
|
// FormatAddresses formats a list of addresses into a human readable string
|
2020-11-10 18:57:09 +00:00
|
|
|
func FormatAddresses(l []*mail.Address) string {
|
2020-08-19 10:01:45 +00:00
|
|
|
formatted := make([]string, len(l))
|
|
|
|
for i, a := range l {
|
2020-11-10 18:57:09 +00:00
|
|
|
formatted[i] = AddressForHumans(a)
|
2020-08-19 10:01:45 +00:00
|
|
|
}
|
|
|
|
return strings.Join(formatted, ", ")
|
2019-11-12 11:50:00 +00:00
|
|
|
}
|
|
|
|
|
2020-10-14 06:42:26 +00:00
|
|
|
type Ctx struct {
|
|
|
|
FromAddress string
|
|
|
|
AccountName string
|
|
|
|
MsgNum int
|
|
|
|
MsgInfo *models.MessageInfo
|
|
|
|
MsgIsMarked bool
|
2021-11-12 17:12:02 +00:00
|
|
|
|
|
|
|
// UI controls for threading
|
|
|
|
ThreadPrefix string
|
|
|
|
ThreadSameSubject bool
|
2020-10-14 06:42:26 +00:00
|
|
|
}
|
|
|
|
|
2021-10-26 15:24:45 +00:00
|
|
|
func ParseMessageFormat(format string, timeFmt string, thisDayTimeFmt string,
|
2021-11-06 16:32:38 +00:00
|
|
|
thisWeekTimeFmt string, thisYearTimeFmt string, ctx Ctx) (
|
|
|
|
string, []interface{}, error) {
|
2019-06-07 19:35:23 +00:00
|
|
|
retval := make([]byte, 0, len(format))
|
|
|
|
var args []interface{}
|
|
|
|
|
2020-11-10 19:35:47 +00:00
|
|
|
accountFromAddress, err := mail.ParseAddress(ctx.FromAddress)
|
2020-08-19 10:01:45 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", nil, err
|
|
|
|
}
|
2019-11-12 11:50:00 +00:00
|
|
|
|
2020-10-14 06:42:26 +00:00
|
|
|
envelope := ctx.MsgInfo.Envelope
|
2021-10-28 12:59:54 +00:00
|
|
|
if envelope == nil {
|
|
|
|
return "", nil,
|
|
|
|
errors.New("no envelope available for this message")
|
|
|
|
}
|
2020-10-14 06:42:26 +00:00
|
|
|
|
2019-06-07 19:35:23 +00:00
|
|
|
var c rune
|
|
|
|
for i, ni := 0, 0; i < len(format); {
|
|
|
|
ni = strings.IndexByte(format[i:], '%')
|
|
|
|
if ni < 0 {
|
|
|
|
ni = len(format)
|
|
|
|
retval = append(retval, []byte(format[i:ni])...)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
ni += i + 1
|
|
|
|
// Check for fmt flags
|
|
|
|
if ni == len(format) {
|
|
|
|
goto handle_end_error
|
|
|
|
}
|
|
|
|
c = rune(format[ni])
|
|
|
|
if c == '+' || c == '-' || c == '#' || c == ' ' || c == '0' {
|
|
|
|
ni++
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for precision and width
|
|
|
|
if ni == len(format) {
|
|
|
|
goto handle_end_error
|
|
|
|
}
|
|
|
|
c = rune(format[ni])
|
|
|
|
for unicode.IsDigit(c) {
|
|
|
|
ni++
|
|
|
|
c = rune(format[ni])
|
|
|
|
}
|
|
|
|
if c == '.' {
|
|
|
|
ni++
|
|
|
|
c = rune(format[ni])
|
|
|
|
for unicode.IsDigit(c) {
|
|
|
|
ni++
|
|
|
|
c = rune(format[ni])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
retval = append(retval, []byte(format[i:ni])...)
|
|
|
|
// Get final format verb
|
|
|
|
if ni == len(format) {
|
|
|
|
goto handle_end_error
|
|
|
|
}
|
|
|
|
c = rune(format[ni])
|
|
|
|
switch c {
|
|
|
|
case '%':
|
|
|
|
retval = append(retval, '%')
|
|
|
|
case 'a':
|
2020-10-14 06:42:26 +00:00
|
|
|
if len(envelope.From) == 0 {
|
2019-07-21 20:01:51 +00:00
|
|
|
return "", nil,
|
|
|
|
errors.New("found no address for sender")
|
2019-06-07 19:35:23 +00:00
|
|
|
}
|
2020-10-14 06:42:26 +00:00
|
|
|
addr := envelope.From[0]
|
2019-06-07 19:35:23 +00:00
|
|
|
retval = append(retval, 's')
|
2020-08-19 10:01:45 +00:00
|
|
|
args = append(args, addr.Address)
|
2019-06-07 19:35:23 +00:00
|
|
|
case 'A':
|
2020-11-10 18:57:09 +00:00
|
|
|
var addr *mail.Address
|
2020-10-14 06:42:26 +00:00
|
|
|
if len(envelope.ReplyTo) == 0 {
|
|
|
|
if len(envelope.From) == 0 {
|
2019-06-07 19:35:23 +00:00
|
|
|
return "", nil,
|
|
|
|
errors.New("found no address for sender or reply-to")
|
|
|
|
} else {
|
2020-10-14 06:42:26 +00:00
|
|
|
addr = envelope.From[0]
|
2019-06-07 19:35:23 +00:00
|
|
|
}
|
|
|
|
} else {
|
2020-10-14 06:42:26 +00:00
|
|
|
addr = envelope.ReplyTo[0]
|
2019-06-07 19:35:23 +00:00
|
|
|
}
|
|
|
|
retval = append(retval, 's')
|
2020-08-19 10:01:45 +00:00
|
|
|
args = append(args, addr.Address)
|
2019-06-07 19:35:23 +00:00
|
|
|
case 'C':
|
|
|
|
retval = append(retval, 'd')
|
2020-10-14 06:42:26 +00:00
|
|
|
args = append(args, ctx.MsgNum)
|
2019-06-07 19:35:23 +00:00
|
|
|
case 'd':
|
2020-10-14 06:42:26 +00:00
|
|
|
date := envelope.Date
|
2020-07-26 23:48:22 +00:00
|
|
|
if date.IsZero() {
|
2020-10-14 06:42:26 +00:00
|
|
|
date = ctx.MsgInfo.InternalDate
|
2020-07-26 23:48:22 +00:00
|
|
|
}
|
2019-06-07 19:35:23 +00:00
|
|
|
retval = append(retval, 's')
|
2019-07-21 20:01:51 +00:00
|
|
|
args = append(args,
|
2021-10-26 15:24:45 +00:00
|
|
|
dummyIfZeroDate(date.Local(),
|
2021-11-06 16:32:38 +00:00
|
|
|
timeFmt, thisDayTimeFmt,
|
|
|
|
thisWeekTimeFmt, thisYearTimeFmt))
|
2019-06-07 19:35:23 +00:00
|
|
|
case 'D':
|
2020-10-14 06:42:26 +00:00
|
|
|
date := envelope.Date
|
2020-07-26 23:48:22 +00:00
|
|
|
if date.IsZero() {
|
2020-10-14 06:42:26 +00:00
|
|
|
date = ctx.MsgInfo.InternalDate
|
2020-07-26 23:48:22 +00:00
|
|
|
}
|
2019-06-07 19:35:23 +00:00
|
|
|
retval = append(retval, 's')
|
2019-07-21 20:01:51 +00:00
|
|
|
args = append(args,
|
2021-10-26 15:24:45 +00:00
|
|
|
dummyIfZeroDate(date.Local(),
|
2021-11-06 16:32:38 +00:00
|
|
|
timeFmt, thisDayTimeFmt,
|
|
|
|
thisWeekTimeFmt, thisYearTimeFmt))
|
2019-06-07 19:35:23 +00:00
|
|
|
case 'f':
|
2020-10-14 06:42:26 +00:00
|
|
|
if len(envelope.From) == 0 {
|
2019-07-21 20:01:51 +00:00
|
|
|
return "", nil,
|
|
|
|
errors.New("found no address for sender")
|
2019-06-07 19:35:23 +00:00
|
|
|
}
|
2020-11-10 18:57:09 +00:00
|
|
|
addr := AddressForHumans(envelope.From[0])
|
2019-06-07 19:35:23 +00:00
|
|
|
retval = append(retval, 's')
|
|
|
|
args = append(args, addr)
|
|
|
|
case 'F':
|
2020-10-14 06:42:26 +00:00
|
|
|
if len(envelope.From) == 0 {
|
2019-07-21 20:01:51 +00:00
|
|
|
return "", nil,
|
|
|
|
errors.New("found no address for sender")
|
2019-06-07 19:35:23 +00:00
|
|
|
}
|
2020-10-14 06:42:26 +00:00
|
|
|
addr := envelope.From[0]
|
2019-06-07 19:35:23 +00:00
|
|
|
var val string
|
2019-11-12 11:50:00 +00:00
|
|
|
|
2020-10-14 06:42:26 +00:00
|
|
|
if addr.Name == accountFromAddress.Name && len(envelope.To) != 0 {
|
|
|
|
addr = envelope.To[0]
|
2019-11-12 11:50:00 +00:00
|
|
|
}
|
|
|
|
|
2019-07-08 02:43:58 +00:00
|
|
|
if addr.Name != "" {
|
|
|
|
val = addr.Name
|
2019-06-07 19:35:23 +00:00
|
|
|
} else {
|
2020-08-19 10:01:45 +00:00
|
|
|
val = addr.Address
|
2019-06-07 19:35:23 +00:00
|
|
|
}
|
|
|
|
retval = append(retval, 's')
|
|
|
|
args = append(args, val)
|
|
|
|
|
2019-12-23 11:51:58 +00:00
|
|
|
case 'g':
|
|
|
|
retval = append(retval, 's')
|
2020-10-14 06:42:26 +00:00
|
|
|
args = append(args, strings.Join(ctx.MsgInfo.Labels, ", "))
|
2019-12-23 11:51:58 +00:00
|
|
|
|
2019-06-07 19:35:23 +00:00
|
|
|
case 'i':
|
|
|
|
retval = append(retval, 's')
|
2020-10-14 06:42:26 +00:00
|
|
|
args = append(args, envelope.MessageId)
|
2019-06-07 19:35:23 +00:00
|
|
|
case 'n':
|
2020-10-14 06:42:26 +00:00
|
|
|
if len(envelope.From) == 0 {
|
2019-07-21 20:01:51 +00:00
|
|
|
return "", nil,
|
|
|
|
errors.New("found no address for sender")
|
2019-06-07 19:35:23 +00:00
|
|
|
}
|
2020-10-14 06:42:26 +00:00
|
|
|
addr := envelope.From[0]
|
2019-06-07 19:35:23 +00:00
|
|
|
var val string
|
2019-07-08 02:43:58 +00:00
|
|
|
if addr.Name != "" {
|
|
|
|
val = addr.Name
|
2019-06-07 19:35:23 +00:00
|
|
|
} else {
|
2020-08-19 10:01:45 +00:00
|
|
|
val = addr.Address
|
2019-06-07 19:35:23 +00:00
|
|
|
}
|
|
|
|
retval = append(retval, 's')
|
|
|
|
args = append(args, val)
|
|
|
|
case 'r':
|
2020-10-14 06:42:26 +00:00
|
|
|
addrs := FormatAddresses(envelope.To)
|
2019-06-07 19:35:23 +00:00
|
|
|
retval = append(retval, 's')
|
|
|
|
args = append(args, addrs)
|
|
|
|
case 'R':
|
2020-10-14 06:42:26 +00:00
|
|
|
addrs := FormatAddresses(envelope.Cc)
|
2019-06-07 19:35:23 +00:00
|
|
|
retval = append(retval, 's')
|
|
|
|
args = append(args, addrs)
|
|
|
|
case 's':
|
|
|
|
retval = append(retval, 's')
|
2021-11-12 17:12:02 +00:00
|
|
|
// if we are threaded strip the repeated subjects unless it's the
|
|
|
|
// first on the screen
|
|
|
|
subject := envelope.Subject
|
|
|
|
if ctx.ThreadSameSubject {
|
|
|
|
subject = ""
|
|
|
|
}
|
|
|
|
args = append(args, ctx.ThreadPrefix+subject)
|
2019-07-21 20:01:51 +00:00
|
|
|
case 't':
|
2020-10-14 06:42:26 +00:00
|
|
|
if len(envelope.To) == 0 {
|
2019-07-21 20:01:51 +00:00
|
|
|
return "", nil,
|
|
|
|
errors.New("found no address for recipient")
|
|
|
|
}
|
2020-10-14 06:42:26 +00:00
|
|
|
addr := envelope.To[0]
|
2019-07-21 20:01:51 +00:00
|
|
|
retval = append(retval, 's')
|
2020-08-19 10:01:45 +00:00
|
|
|
args = append(args, addr.Address)
|
2019-07-21 20:01:51 +00:00
|
|
|
case 'T':
|
|
|
|
retval = append(retval, 's')
|
2020-10-14 06:42:26 +00:00
|
|
|
args = append(args, ctx.AccountName)
|
2019-06-07 19:35:23 +00:00
|
|
|
case 'u':
|
2020-10-14 06:42:26 +00:00
|
|
|
if len(envelope.From) == 0 {
|
2019-07-21 20:01:51 +00:00
|
|
|
return "", nil,
|
|
|
|
errors.New("found no address for sender")
|
2019-06-07 19:35:23 +00:00
|
|
|
}
|
2020-10-14 06:42:26 +00:00
|
|
|
addr := envelope.From[0]
|
2020-08-19 10:01:45 +00:00
|
|
|
mailbox := addr.Address // fallback if there's no @ sign
|
|
|
|
if split := strings.SplitN(addr.Address, "@", 2); len(split) == 2 {
|
|
|
|
mailbox = split[1]
|
|
|
|
}
|
2019-06-07 19:35:23 +00:00
|
|
|
retval = append(retval, 's')
|
2020-08-19 10:01:45 +00:00
|
|
|
args = append(args, mailbox)
|
2019-06-07 19:35:23 +00:00
|
|
|
case 'v':
|
2020-10-14 06:42:26 +00:00
|
|
|
if len(envelope.From) == 0 {
|
2019-07-21 20:01:51 +00:00
|
|
|
return "", nil,
|
|
|
|
errors.New("found no address for sender")
|
2019-06-07 19:35:23 +00:00
|
|
|
}
|
2020-10-14 06:42:26 +00:00
|
|
|
addr := envelope.From[0]
|
2019-06-07 19:35:23 +00:00
|
|
|
// check if message is from current user
|
2019-07-08 02:43:58 +00:00
|
|
|
if addr.Name != "" {
|
2019-06-07 19:35:23 +00:00
|
|
|
retval = append(retval, 's')
|
2019-07-21 20:01:51 +00:00
|
|
|
args = append(args,
|
|
|
|
strings.Split(addr.Name, " ")[0])
|
2019-06-07 19:35:23 +00:00
|
|
|
}
|
|
|
|
case 'Z':
|
|
|
|
// calculate all flags
|
2019-07-12 15:14:26 +00:00
|
|
|
var readReplyFlag = ""
|
2019-06-07 19:35:23 +00:00
|
|
|
var delFlag = ""
|
|
|
|
var flaggedFlag = ""
|
2019-12-18 05:34:07 +00:00
|
|
|
var markedFlag = ""
|
2019-07-12 15:14:26 +00:00
|
|
|
seen := false
|
|
|
|
recent := false
|
|
|
|
answered := false
|
2020-10-14 06:42:26 +00:00
|
|
|
for _, flag := range ctx.MsgInfo.Flags {
|
2019-07-08 02:43:58 +00:00
|
|
|
if flag == models.SeenFlag {
|
2019-07-12 15:14:26 +00:00
|
|
|
seen = true
|
2019-07-08 02:43:58 +00:00
|
|
|
} else if flag == models.RecentFlag {
|
2019-07-12 15:14:26 +00:00
|
|
|
recent = true
|
2019-07-08 02:43:58 +00:00
|
|
|
} else if flag == models.AnsweredFlag {
|
2019-07-12 15:14:26 +00:00
|
|
|
answered = true
|
2019-06-14 21:11:17 +00:00
|
|
|
}
|
2019-07-08 02:43:58 +00:00
|
|
|
if flag == models.DeletedFlag {
|
2019-06-07 19:35:23 +00:00
|
|
|
delFlag = "D"
|
|
|
|
// TODO: check if attachments
|
2019-06-14 21:11:17 +00:00
|
|
|
}
|
2019-07-08 02:43:58 +00:00
|
|
|
if flag == models.FlaggedFlag {
|
2019-06-07 19:35:23 +00:00
|
|
|
flaggedFlag = "!"
|
|
|
|
}
|
|
|
|
// TODO: check gpg stuff
|
|
|
|
}
|
2019-07-12 15:14:26 +00:00
|
|
|
if seen {
|
|
|
|
if answered {
|
|
|
|
readReplyFlag = "r" // message has been replied to
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if recent {
|
|
|
|
readReplyFlag = "N" // message is new
|
|
|
|
} else {
|
|
|
|
readReplyFlag = "O" // message is old
|
|
|
|
}
|
|
|
|
}
|
2020-10-14 06:42:26 +00:00
|
|
|
if ctx.MsgIsMarked {
|
2019-12-18 05:34:07 +00:00
|
|
|
markedFlag = "*"
|
|
|
|
}
|
2019-12-22 12:08:23 +00:00
|
|
|
retval = append(retval, '4', 's')
|
2019-12-18 05:34:07 +00:00
|
|
|
args = append(args, readReplyFlag+delFlag+flaggedFlag+markedFlag)
|
2019-06-07 19:35:23 +00:00
|
|
|
|
|
|
|
// Move the below cases to proper alphabetical positions once
|
|
|
|
// implemented
|
|
|
|
case 'l':
|
|
|
|
// TODO: number of lines in the message
|
|
|
|
retval = append(retval, 'd')
|
2020-10-14 06:42:26 +00:00
|
|
|
args = append(args, ctx.MsgInfo.Size)
|
2019-06-07 19:35:23 +00:00
|
|
|
case 'e':
|
|
|
|
// TODO: current message number in thread
|
|
|
|
fallthrough
|
|
|
|
case 'E':
|
|
|
|
// TODO: number of messages in current thread
|
|
|
|
fallthrough
|
|
|
|
case 'H':
|
|
|
|
// TODO: spam attribute(s) of this message
|
|
|
|
fallthrough
|
|
|
|
case 'L':
|
|
|
|
// TODO:
|
|
|
|
fallthrough
|
|
|
|
case 'X':
|
|
|
|
// TODO: number of attachments
|
|
|
|
fallthrough
|
|
|
|
case 'y':
|
|
|
|
// TODO: X-Label field
|
|
|
|
fallthrough
|
|
|
|
case 'Y':
|
|
|
|
// TODO: X-Label field and some other constraints
|
|
|
|
fallthrough
|
|
|
|
default:
|
|
|
|
// Just ignore it and print as is
|
|
|
|
// so %k in index format becomes %%k to Printf
|
|
|
|
retval = append(retval, '%')
|
|
|
|
retval = append(retval, byte(c))
|
|
|
|
}
|
|
|
|
i = ni + 1
|
|
|
|
}
|
|
|
|
|
|
|
|
return string(retval), args, nil
|
|
|
|
|
|
|
|
handle_end_error:
|
2019-07-21 20:01:51 +00:00
|
|
|
return "", nil,
|
|
|
|
errors.New("reached end of string while parsing message format")
|
2019-06-07 19:35:23 +00:00
|
|
|
}
|
2020-07-26 23:48:22 +00:00
|
|
|
|
2021-10-26 15:24:45 +00:00
|
|
|
func dummyIfZeroDate(date time.Time, format string, todayFormat string,
|
2021-11-06 16:32:38 +00:00
|
|
|
thisWeekFormat string, thisYearFormat string) string {
|
2020-07-26 23:48:22 +00:00
|
|
|
if date.IsZero() {
|
|
|
|
return strings.Repeat("?", len(format))
|
|
|
|
}
|
2021-11-06 16:32:38 +00:00
|
|
|
year := date.Year()
|
|
|
|
day := date.YearDay()
|
|
|
|
now := time.Now()
|
|
|
|
thisYear := now.Year()
|
|
|
|
thisDay := now.YearDay()
|
2021-10-26 15:24:45 +00:00
|
|
|
if year == thisYear {
|
2021-11-06 16:32:38 +00:00
|
|
|
if day == thisDay && todayFormat != "" {
|
2021-10-26 15:24:45 +00:00
|
|
|
return date.Format(todayFormat)
|
|
|
|
}
|
2021-11-06 16:32:38 +00:00
|
|
|
if day > thisDay-7 && thisWeekFormat != "" {
|
|
|
|
return date.Format(thisWeekFormat)
|
|
|
|
}
|
2021-10-26 15:24:45 +00:00
|
|
|
if thisYearFormat != "" {
|
|
|
|
return date.Format(thisYearFormat)
|
|
|
|
}
|
|
|
|
}
|
2020-07-26 23:48:22 +00:00
|
|
|
return date.Format(format)
|
|
|
|
}
|