Copy unsent messages to Outbox
This patch: 1. Copies unsent messages to the outbox before attempting to deliver them with SMTP 2. Deletes those messages once they're sent, or leaves them if an error occured 3. Updates the message list to make it obvious when there are unsent messages in the outbox
This commit is contained in:
parent
d325628cb2
commit
cbeacf9d06
4 changed files with 92 additions and 12 deletions
|
@ -22,6 +22,7 @@ type MailboxInfo struct {
|
|||
*imap.MailboxInfo
|
||||
|
||||
Active bool
|
||||
Total int
|
||||
Unseen int
|
||||
}
|
||||
|
||||
|
@ -49,7 +50,7 @@ func listMailboxes(conn *imapclient.Client) ([]MailboxInfo, error) {
|
|||
|
||||
var mailboxes []MailboxInfo
|
||||
for mbox := range ch {
|
||||
mailboxes = append(mailboxes, MailboxInfo{mbox, false, -1})
|
||||
mailboxes = append(mailboxes, MailboxInfo{mbox, false, -1, -1})
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
|
@ -95,6 +96,7 @@ type mailboxType int
|
|||
|
||||
const (
|
||||
mailboxSent mailboxType = iota
|
||||
mailboxOutbox mailboxType = iota
|
||||
mailboxDrafts
|
||||
)
|
||||
|
||||
|
@ -115,6 +117,9 @@ func getMailboxByType(conn *imapclient.Client, mboxType mailboxType) (*MailboxIn
|
|||
case mailboxDrafts:
|
||||
attr = imapspecialuse.Drafts
|
||||
fallbackNames = []string{"Draft", "Drafts"}
|
||||
case mailboxOutbox:
|
||||
attr = ""
|
||||
fallbackNames = []string{"Outbox"}
|
||||
}
|
||||
|
||||
var attrMatched bool
|
||||
|
@ -146,7 +151,7 @@ func getMailboxByType(conn *imapclient.Client, mboxType mailboxType) (*MailboxIn
|
|||
if best == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return &MailboxInfo{best, false, -1}, nil
|
||||
return &MailboxInfo{best, false, -1, -1}, nil
|
||||
}
|
||||
|
||||
func ensureMailboxSelected(conn *imapclient.Client, mboxName string) error {
|
||||
|
|
|
@ -73,6 +73,7 @@ type IMAPBaseRenderData struct {
|
|||
Mailboxes []MailboxInfo
|
||||
Mailbox *MailboxStatus
|
||||
Inbox *MailboxStatus
|
||||
Outbox *MailboxStatus
|
||||
}
|
||||
|
||||
type MailboxRenderData struct {
|
||||
|
@ -87,6 +88,7 @@ type CategorizedMailboxes struct {
|
|||
Common struct {
|
||||
Inbox *MailboxInfo
|
||||
Drafts *MailboxInfo
|
||||
Outbox *MailboxInfo
|
||||
Sent *MailboxInfo
|
||||
Junk *MailboxInfo
|
||||
Trash *MailboxInfo
|
||||
|
@ -104,7 +106,7 @@ func newIMAPBaseRenderData(ctx *alps.Context,
|
|||
}
|
||||
|
||||
var mailboxes []MailboxInfo
|
||||
var active, inbox *MailboxStatus
|
||||
var active, inbox, outbox *MailboxStatus
|
||||
err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
|
||||
var err error
|
||||
if mailboxes, err = listMailboxes(c); err != nil {
|
||||
|
@ -122,6 +124,13 @@ func newIMAPBaseRenderData(ctx *alps.Context,
|
|||
return err
|
||||
}
|
||||
}
|
||||
if mboxName == "Outbox" {
|
||||
outbox = active
|
||||
} else {
|
||||
if outbox, err = getMailboxStatus(c, "Outbox"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -132,6 +141,7 @@ func newIMAPBaseRenderData(ctx *alps.Context,
|
|||
mmap := map[string]**MailboxInfo{
|
||||
"INBOX": &categorized.Common.Inbox,
|
||||
"Drafts": &categorized.Common.Drafts,
|
||||
"Outbox": &categorized.Common.Outbox,
|
||||
"Sent": &categorized.Common.Sent,
|
||||
"Junk": &categorized.Common.Junk,
|
||||
"Trash": &categorized.Common.Trash,
|
||||
|
@ -142,10 +152,16 @@ func newIMAPBaseRenderData(ctx *alps.Context,
|
|||
// Populate unseen & active states
|
||||
if active != nil && mailboxes[i].Name == active.Name {
|
||||
mailboxes[i].Unseen = int(active.Unseen)
|
||||
mailboxes[i].Total = int(active.Messages)
|
||||
mailboxes[i].Active = true
|
||||
}
|
||||
if mailboxes[i].Name == inbox.Name {
|
||||
mailboxes[i].Unseen = int(inbox.Unseen)
|
||||
mailboxes[i].Total = int(inbox.Messages)
|
||||
}
|
||||
if mailboxes[i].Name == outbox.Name {
|
||||
mailboxes[i].Unseen = int(outbox.Unseen)
|
||||
mailboxes[i].Total = int(outbox.Messages)
|
||||
}
|
||||
|
||||
if ptr, ok := mmap[mailboxes[i].Name]; ok {
|
||||
|
@ -416,8 +432,23 @@ type composeOptions struct {
|
|||
// Send message, append it to the Sent mailbox, mark the original message as
|
||||
// answered
|
||||
func submitCompose(ctx *alps.Context, msg *OutgoingMessage, options *composeOptions) error {
|
||||
msg.Ref()
|
||||
msg.Ref()
|
||||
msg.Ref(3)
|
||||
|
||||
err := ctx.Session.DoIMAP(func(c *imapclient.Client) error {
|
||||
// (disregard error, we don't care if Outbox already existed)
|
||||
c.Create("Outbox")
|
||||
|
||||
if _, err := appendMessage(c, msg, mailboxOutbox); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg.Unref()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save message to outbox: %v", err)
|
||||
}
|
||||
|
||||
task := work.NewTask(func(_ context.Context) error {
|
||||
err := ctx.Session.DoSMTP(func (c *smtp.Client) error {
|
||||
return sendMessage(c, msg)
|
||||
|
@ -427,9 +458,43 @@ func submitCompose(ctx *alps.Context, msg *OutgoingMessage, options *composeOpti
|
|||
}
|
||||
return err
|
||||
}).Retries(5).After(func(_ context.Context, task *work.Task) {
|
||||
ctx.Logger().Printf("email sent: %v", task.Result())
|
||||
if task.Result() == nil {
|
||||
// Remove from outbox
|
||||
err := ctx.Session.DoIMAP(func(c *imapclient.Client) error {
|
||||
ctx.Logger().Printf("DoIMAP")
|
||||
if err := ensureMailboxSelected(c, "Outbox"); err != nil {
|
||||
return err
|
||||
}
|
||||
uids, err := c.UidSearch(&imap.SearchCriteria{
|
||||
Header: map[string][]string{
|
||||
"Message-Id": []string{msg.MessageID},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("UID SEARCH failed: %v", err)
|
||||
}
|
||||
if len(uids) == 1 {
|
||||
if err = deleteMessage(c, "Outbox", uids[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
ctx.Logger().Errorf(
|
||||
"Unexpectedly found multiple results in outbox for message ID %s",
|
||||
msg.MessageID)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Logger().Errorf("Error removing message from outbox: %v", err)
|
||||
}
|
||||
} else {
|
||||
ctx.Logger().Errorf("Message delivery failed with error %v", err)
|
||||
}
|
||||
|
||||
msg.Unref()
|
||||
})
|
||||
err := ctx.Server.Queue.Enqueue(task)
|
||||
err = ctx.Server.Queue.Enqueue(task)
|
||||
if err != nil {
|
||||
if _, ok := err.(alps.AuthError); ok {
|
||||
return echo.NewHTTPError(http.StatusForbidden, err)
|
||||
|
@ -451,6 +516,7 @@ func submitCompose(ctx *alps.Context, msg *OutgoingMessage, options *composeOpti
|
|||
return err
|
||||
}
|
||||
msg.Unref()
|
||||
|
||||
if draft := options.Draft; draft != nil {
|
||||
if err := deleteMessage(c, draft.Mailbox, draft.Uid); err != nil {
|
||||
return err
|
||||
|
@ -599,6 +665,7 @@ func handleComposeNew(ctx *alps.Context) error {
|
|||
To: strings.Split(ctx.QueryParam("to"), ","),
|
||||
Subject: ctx.QueryParam("subject"),
|
||||
Text: ctx.QueryParam("body"),
|
||||
MessageID: mail.GenerateMessageID(),
|
||||
InReplyTo: ctx.QueryParam("in-reply-to"),
|
||||
}, &composeOptions{})
|
||||
}
|
||||
|
@ -676,6 +743,7 @@ func handleReply(ctx *alps.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
msg.MessageID = mail.GenerateMessageID()
|
||||
msg.InReplyTo = inReplyTo.Envelope.MessageId
|
||||
// TODO: populate From from known user addresses and inReplyTo.Envelope.To
|
||||
replyTo := inReplyTo.Envelope.ReplyTo
|
||||
|
@ -734,6 +802,7 @@ func handleForward(ctx *alps.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
msg.MessageID = mail.GenerateMessageID()
|
||||
msg.Subject = source.Envelope.Subject
|
||||
if !strings.HasPrefix(strings.ToLower(msg.Subject), "fwd:") &&
|
||||
!strings.HasPrefix(strings.ToLower(msg.Subject), "fw:") {
|
||||
|
@ -807,7 +876,7 @@ func handleEdit(ctx *alps.Context) error {
|
|||
msg.To = unwrapIMAPAddressList(source.Envelope.To)
|
||||
msg.Subject = source.Envelope.Subject
|
||||
msg.InReplyTo = source.Envelope.InReplyTo
|
||||
// TODO: preserve Message-Id
|
||||
msg.MessageID = source.Envelope.MessageId
|
||||
|
||||
attachments := source.Attachments()
|
||||
for i := range attachments {
|
||||
|
|
|
@ -73,8 +73,8 @@ func (att *refcountedAttachment) Filename() string {
|
|||
return att.FileHeader.Filename
|
||||
}
|
||||
|
||||
func (att *refcountedAttachment) Ref() {
|
||||
att.refs += 1
|
||||
func (att *refcountedAttachment) Ref(n int) {
|
||||
att.refs += n
|
||||
}
|
||||
|
||||
func (att *refcountedAttachment) Unref() {
|
||||
|
@ -111,6 +111,7 @@ type OutgoingMessage struct {
|
|||
From string
|
||||
To []string
|
||||
Subject string
|
||||
MessageID string
|
||||
InReplyTo string
|
||||
Text string
|
||||
Attachments []Attachment
|
||||
|
@ -170,7 +171,7 @@ func (msg *OutgoingMessage) WriteTo(w io.Writer) error {
|
|||
h.Set("In-Reply-To", msg.InReplyTo)
|
||||
}
|
||||
|
||||
h.Set("Message-Id", mail.GenerateMessageID())
|
||||
h.Set("Message-Id", msg.MessageID)
|
||||
|
||||
mw, err := mail.CreateWriter(w, h)
|
||||
if err != nil {
|
||||
|
@ -207,10 +208,10 @@ func (msg *OutgoingMessage) WriteTo(w io.Writer) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (msg *OutgoingMessage) Ref() {
|
||||
func (msg *OutgoingMessage) Ref(n int) {
|
||||
for _, a := range msg.Attachments {
|
||||
if a, ok := a.(*refcountedAttachment); ok {
|
||||
a.Ref()
|
||||
a.Ref(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,11 @@
|
|||
{{- end -}}
|
||||
{{- if .HasAttr "\\HasChildren" }}/{{ end }}
|
||||
|
||||
{{ if eq .Name "Outbox" }}
|
||||
{{ if and (ne .Total -1) (ne .Total 0) }}({{ .Total }} unsent){{ end }}
|
||||
{{ else }}
|
||||
{{ if and (ne .Unseen -1) (ne .Unseen 0) }}({{ .Unseen }}){{ end }}
|
||||
{{ end }}
|
||||
</a>
|
||||
{{ else }}
|
||||
<span class="noselect">
|
||||
|
@ -26,6 +30,7 @@
|
|||
{{ with .CategorizedMailboxes }}
|
||||
{{ with .Common.Inbox }}{{ template "mbox-link" . }}{{ end}}
|
||||
{{ with .Common.Drafts }}{{ template "mbox-link" . }}{{ end}}
|
||||
{{ with .Common.Outbox }}{{ template "mbox-link" . }}{{ end}}
|
||||
{{ with .Common.Sent }}{{ template "mbox-link" . }}{{ end}}
|
||||
{{ with .Common.Junk }}{{ template "mbox-link" . }}{{ end}}
|
||||
{{ with .Common.Trash }}{{ template "mbox-link" . }}{{ end}}
|
||||
|
|
Loading…
Reference in a new issue