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
|
*imap.MailboxInfo
|
||||||
|
|
||||||
Active bool
|
Active bool
|
||||||
|
Total int
|
||||||
Unseen int
|
Unseen int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +50,7 @@ func listMailboxes(conn *imapclient.Client) ([]MailboxInfo, error) {
|
||||||
|
|
||||||
var mailboxes []MailboxInfo
|
var mailboxes []MailboxInfo
|
||||||
for mbox := range ch {
|
for mbox := range ch {
|
||||||
mailboxes = append(mailboxes, MailboxInfo{mbox, false, -1})
|
mailboxes = append(mailboxes, MailboxInfo{mbox, false, -1, -1})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := <-done; err != nil {
|
if err := <-done; err != nil {
|
||||||
|
@ -95,6 +96,7 @@ type mailboxType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
mailboxSent mailboxType = iota
|
mailboxSent mailboxType = iota
|
||||||
|
mailboxOutbox mailboxType = iota
|
||||||
mailboxDrafts
|
mailboxDrafts
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -115,6 +117,9 @@ func getMailboxByType(conn *imapclient.Client, mboxType mailboxType) (*MailboxIn
|
||||||
case mailboxDrafts:
|
case mailboxDrafts:
|
||||||
attr = imapspecialuse.Drafts
|
attr = imapspecialuse.Drafts
|
||||||
fallbackNames = []string{"Draft", "Drafts"}
|
fallbackNames = []string{"Draft", "Drafts"}
|
||||||
|
case mailboxOutbox:
|
||||||
|
attr = ""
|
||||||
|
fallbackNames = []string{"Outbox"}
|
||||||
}
|
}
|
||||||
|
|
||||||
var attrMatched bool
|
var attrMatched bool
|
||||||
|
@ -146,7 +151,7 @@ func getMailboxByType(conn *imapclient.Client, mboxType mailboxType) (*MailboxIn
|
||||||
if best == nil {
|
if best == nil {
|
||||||
return nil, 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 {
|
func ensureMailboxSelected(conn *imapclient.Client, mboxName string) error {
|
||||||
|
|
|
@ -73,6 +73,7 @@ type IMAPBaseRenderData struct {
|
||||||
Mailboxes []MailboxInfo
|
Mailboxes []MailboxInfo
|
||||||
Mailbox *MailboxStatus
|
Mailbox *MailboxStatus
|
||||||
Inbox *MailboxStatus
|
Inbox *MailboxStatus
|
||||||
|
Outbox *MailboxStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
type MailboxRenderData struct {
|
type MailboxRenderData struct {
|
||||||
|
@ -87,6 +88,7 @@ type CategorizedMailboxes struct {
|
||||||
Common struct {
|
Common struct {
|
||||||
Inbox *MailboxInfo
|
Inbox *MailboxInfo
|
||||||
Drafts *MailboxInfo
|
Drafts *MailboxInfo
|
||||||
|
Outbox *MailboxInfo
|
||||||
Sent *MailboxInfo
|
Sent *MailboxInfo
|
||||||
Junk *MailboxInfo
|
Junk *MailboxInfo
|
||||||
Trash *MailboxInfo
|
Trash *MailboxInfo
|
||||||
|
@ -104,7 +106,7 @@ func newIMAPBaseRenderData(ctx *alps.Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
var mailboxes []MailboxInfo
|
var mailboxes []MailboxInfo
|
||||||
var active, inbox *MailboxStatus
|
var active, inbox, outbox *MailboxStatus
|
||||||
err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
|
err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
|
||||||
var err error
|
var err error
|
||||||
if mailboxes, err = listMailboxes(c); err != nil {
|
if mailboxes, err = listMailboxes(c); err != nil {
|
||||||
|
@ -122,6 +124,13 @@ func newIMAPBaseRenderData(ctx *alps.Context,
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if mboxName == "Outbox" {
|
||||||
|
outbox = active
|
||||||
|
} else {
|
||||||
|
if outbox, err = getMailboxStatus(c, "Outbox"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -132,6 +141,7 @@ func newIMAPBaseRenderData(ctx *alps.Context,
|
||||||
mmap := map[string]**MailboxInfo{
|
mmap := map[string]**MailboxInfo{
|
||||||
"INBOX": &categorized.Common.Inbox,
|
"INBOX": &categorized.Common.Inbox,
|
||||||
"Drafts": &categorized.Common.Drafts,
|
"Drafts": &categorized.Common.Drafts,
|
||||||
|
"Outbox": &categorized.Common.Outbox,
|
||||||
"Sent": &categorized.Common.Sent,
|
"Sent": &categorized.Common.Sent,
|
||||||
"Junk": &categorized.Common.Junk,
|
"Junk": &categorized.Common.Junk,
|
||||||
"Trash": &categorized.Common.Trash,
|
"Trash": &categorized.Common.Trash,
|
||||||
|
@ -142,10 +152,16 @@ func newIMAPBaseRenderData(ctx *alps.Context,
|
||||||
// Populate unseen & active states
|
// Populate unseen & active states
|
||||||
if active != nil && mailboxes[i].Name == active.Name {
|
if active != nil && mailboxes[i].Name == active.Name {
|
||||||
mailboxes[i].Unseen = int(active.Unseen)
|
mailboxes[i].Unseen = int(active.Unseen)
|
||||||
|
mailboxes[i].Total = int(active.Messages)
|
||||||
mailboxes[i].Active = true
|
mailboxes[i].Active = true
|
||||||
}
|
}
|
||||||
if mailboxes[i].Name == inbox.Name {
|
if mailboxes[i].Name == inbox.Name {
|
||||||
mailboxes[i].Unseen = int(inbox.Unseen)
|
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 {
|
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
|
// Send message, append it to the Sent mailbox, mark the original message as
|
||||||
// answered
|
// answered
|
||||||
func submitCompose(ctx *alps.Context, msg *OutgoingMessage, options *composeOptions) error {
|
func submitCompose(ctx *alps.Context, msg *OutgoingMessage, options *composeOptions) error {
|
||||||
msg.Ref()
|
msg.Ref(3)
|
||||||
msg.Ref()
|
|
||||||
|
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 {
|
task := work.NewTask(func(_ context.Context) error {
|
||||||
err := ctx.Session.DoSMTP(func (c *smtp.Client) error {
|
err := ctx.Session.DoSMTP(func (c *smtp.Client) error {
|
||||||
return sendMessage(c, msg)
|
return sendMessage(c, msg)
|
||||||
|
@ -427,9 +458,43 @@ func submitCompose(ctx *alps.Context, msg *OutgoingMessage, options *composeOpti
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}).Retries(5).After(func(_ context.Context, task *work.Task) {
|
}).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()
|
msg.Unref()
|
||||||
})
|
})
|
||||||
err := ctx.Server.Queue.Enqueue(task)
|
err = ctx.Server.Queue.Enqueue(task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(alps.AuthError); ok {
|
if _, ok := err.(alps.AuthError); ok {
|
||||||
return echo.NewHTTPError(http.StatusForbidden, err)
|
return echo.NewHTTPError(http.StatusForbidden, err)
|
||||||
|
@ -451,6 +516,7 @@ func submitCompose(ctx *alps.Context, msg *OutgoingMessage, options *composeOpti
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
msg.Unref()
|
msg.Unref()
|
||||||
|
|
||||||
if draft := options.Draft; draft != nil {
|
if draft := options.Draft; draft != nil {
|
||||||
if err := deleteMessage(c, draft.Mailbox, draft.Uid); err != nil {
|
if err := deleteMessage(c, draft.Mailbox, draft.Uid); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -599,6 +665,7 @@ func handleComposeNew(ctx *alps.Context) error {
|
||||||
To: strings.Split(ctx.QueryParam("to"), ","),
|
To: strings.Split(ctx.QueryParam("to"), ","),
|
||||||
Subject: ctx.QueryParam("subject"),
|
Subject: ctx.QueryParam("subject"),
|
||||||
Text: ctx.QueryParam("body"),
|
Text: ctx.QueryParam("body"),
|
||||||
|
MessageID: mail.GenerateMessageID(),
|
||||||
InReplyTo: ctx.QueryParam("in-reply-to"),
|
InReplyTo: ctx.QueryParam("in-reply-to"),
|
||||||
}, &composeOptions{})
|
}, &composeOptions{})
|
||||||
}
|
}
|
||||||
|
@ -676,6 +743,7 @@ func handleReply(ctx *alps.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
msg.MessageID = mail.GenerateMessageID()
|
||||||
msg.InReplyTo = inReplyTo.Envelope.MessageId
|
msg.InReplyTo = inReplyTo.Envelope.MessageId
|
||||||
// TODO: populate From from known user addresses and inReplyTo.Envelope.To
|
// TODO: populate From from known user addresses and inReplyTo.Envelope.To
|
||||||
replyTo := inReplyTo.Envelope.ReplyTo
|
replyTo := inReplyTo.Envelope.ReplyTo
|
||||||
|
@ -734,6 +802,7 @@ func handleForward(ctx *alps.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
msg.MessageID = mail.GenerateMessageID()
|
||||||
msg.Subject = source.Envelope.Subject
|
msg.Subject = source.Envelope.Subject
|
||||||
if !strings.HasPrefix(strings.ToLower(msg.Subject), "fwd:") &&
|
if !strings.HasPrefix(strings.ToLower(msg.Subject), "fwd:") &&
|
||||||
!strings.HasPrefix(strings.ToLower(msg.Subject), "fw:") {
|
!strings.HasPrefix(strings.ToLower(msg.Subject), "fw:") {
|
||||||
|
@ -807,7 +876,7 @@ func handleEdit(ctx *alps.Context) error {
|
||||||
msg.To = unwrapIMAPAddressList(source.Envelope.To)
|
msg.To = unwrapIMAPAddressList(source.Envelope.To)
|
||||||
msg.Subject = source.Envelope.Subject
|
msg.Subject = source.Envelope.Subject
|
||||||
msg.InReplyTo = source.Envelope.InReplyTo
|
msg.InReplyTo = source.Envelope.InReplyTo
|
||||||
// TODO: preserve Message-Id
|
msg.MessageID = source.Envelope.MessageId
|
||||||
|
|
||||||
attachments := source.Attachments()
|
attachments := source.Attachments()
|
||||||
for i := range attachments {
|
for i := range attachments {
|
||||||
|
|
|
@ -73,8 +73,8 @@ func (att *refcountedAttachment) Filename() string {
|
||||||
return att.FileHeader.Filename
|
return att.FileHeader.Filename
|
||||||
}
|
}
|
||||||
|
|
||||||
func (att *refcountedAttachment) Ref() {
|
func (att *refcountedAttachment) Ref(n int) {
|
||||||
att.refs += 1
|
att.refs += n
|
||||||
}
|
}
|
||||||
|
|
||||||
func (att *refcountedAttachment) Unref() {
|
func (att *refcountedAttachment) Unref() {
|
||||||
|
@ -111,6 +111,7 @@ type OutgoingMessage struct {
|
||||||
From string
|
From string
|
||||||
To []string
|
To []string
|
||||||
Subject string
|
Subject string
|
||||||
|
MessageID string
|
||||||
InReplyTo string
|
InReplyTo string
|
||||||
Text string
|
Text string
|
||||||
Attachments []Attachment
|
Attachments []Attachment
|
||||||
|
@ -170,7 +171,7 @@ func (msg *OutgoingMessage) WriteTo(w io.Writer) error {
|
||||||
h.Set("In-Reply-To", msg.InReplyTo)
|
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)
|
mw, err := mail.CreateWriter(w, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -207,10 +208,10 @@ func (msg *OutgoingMessage) WriteTo(w io.Writer) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msg *OutgoingMessage) Ref() {
|
func (msg *OutgoingMessage) Ref(n int) {
|
||||||
for _, a := range msg.Attachments {
|
for _, a := range msg.Attachments {
|
||||||
if a, ok := a.(*refcountedAttachment); ok {
|
if a, ok := a.(*refcountedAttachment); ok {
|
||||||
a.Ref()
|
a.Ref(n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,11 @@
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- if .HasAttr "\\HasChildren" }}/{{ 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 }}
|
{{ if and (ne .Unseen -1) (ne .Unseen 0) }}({{ .Unseen }}){{ end }}
|
||||||
|
{{ end }}
|
||||||
</a>
|
</a>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<span class="noselect">
|
<span class="noselect">
|
||||||
|
@ -26,6 +30,7 @@
|
||||||
{{ with .CategorizedMailboxes }}
|
{{ with .CategorizedMailboxes }}
|
||||||
{{ with .Common.Inbox }}{{ template "mbox-link" . }}{{ end}}
|
{{ with .Common.Inbox }}{{ template "mbox-link" . }}{{ end}}
|
||||||
{{ with .Common.Drafts }}{{ 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.Sent }}{{ template "mbox-link" . }}{{ end}}
|
||||||
{{ with .Common.Junk }}{{ template "mbox-link" . }}{{ end}}
|
{{ with .Common.Junk }}{{ template "mbox-link" . }}{{ end}}
|
||||||
{{ with .Common.Trash }}{{ template "mbox-link" . }}{{ end}}
|
{{ with .Common.Trash }}{{ template "mbox-link" . }}{{ end}}
|
||||||
|
|
Loading…
Reference in a new issue