mirror of
https://github.com/ProtonMail/go-proton-api.git
synced 2025-12-23 23:57:50 -05:00
386 lines
9.2 KiB
Go
386 lines
9.2 KiB
Go
package backend
|
|
|
|
import (
|
|
"net/mail"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/ProtonMail/gluon/rfc822"
|
|
"github.com/ProtonMail/go-proton-api"
|
|
"github.com/bradenaw/juniper/xslices"
|
|
"github.com/google/uuid"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
type message struct {
|
|
messageID string
|
|
externalID string
|
|
addrID string
|
|
labelIDs []string
|
|
attIDs []string
|
|
inReplyTo string
|
|
|
|
// sysLabel is the system label for the message.
|
|
// If nil, the message's flags are used to determine the system label (inbox, sent, drafts).
|
|
// If "", the message has no system label (e.g. is in a custom folder or all mail).
|
|
// If non-nil and non-empty, the message has the system label with the given ID (e.g. spam, trash).
|
|
sysLabel *string
|
|
|
|
subject string
|
|
sender *mail.Address
|
|
toList []*mail.Address
|
|
ccList []*mail.Address
|
|
bccList []*mail.Address
|
|
replytos []*mail.Address
|
|
date time.Time
|
|
|
|
armBody string
|
|
mimeType rfc822.MIMEType
|
|
|
|
flags proton.MessageFlag
|
|
unread bool
|
|
starred bool
|
|
}
|
|
|
|
func newMessage(
|
|
addrID string,
|
|
subject string,
|
|
sender *mail.Address,
|
|
toList, ccList, bccList, replytos []*mail.Address,
|
|
armBody string,
|
|
mimeType rfc822.MIMEType,
|
|
externalID string,
|
|
date time.Time,
|
|
) *message {
|
|
return &message{
|
|
messageID: uuid.NewString(),
|
|
externalID: externalID,
|
|
addrID: addrID,
|
|
sysLabel: pointer(""),
|
|
|
|
subject: subject,
|
|
sender: sender,
|
|
toList: toList,
|
|
ccList: ccList,
|
|
bccList: bccList,
|
|
replytos: replytos,
|
|
date: date,
|
|
|
|
armBody: armBody,
|
|
mimeType: mimeType,
|
|
}
|
|
}
|
|
|
|
func newMessageFromSent(addrID, armBody string, msg *message) *message {
|
|
return &message{
|
|
messageID: uuid.NewString(),
|
|
externalID: msg.externalID,
|
|
addrID: addrID,
|
|
sysLabel: pointer(""),
|
|
|
|
subject: msg.subject,
|
|
sender: msg.sender,
|
|
toList: msg.toList,
|
|
ccList: msg.ccList,
|
|
bccList: nil, // BCC is not sent to the recipient
|
|
replytos: msg.replytos,
|
|
date: time.Now(),
|
|
|
|
armBody: armBody,
|
|
mimeType: msg.mimeType,
|
|
inReplyTo: msg.inReplyTo,
|
|
}
|
|
}
|
|
|
|
func newMessageFromTemplate(addrID string, template proton.DraftTemplate, parentRef string) *message {
|
|
return &message{
|
|
messageID: uuid.NewString(),
|
|
externalID: template.ExternalID,
|
|
addrID: addrID,
|
|
sysLabel: pointer(""),
|
|
inReplyTo: parentRef,
|
|
|
|
subject: template.Subject,
|
|
sender: template.Sender,
|
|
toList: template.ToList,
|
|
ccList: template.CCList,
|
|
bccList: template.BCCList,
|
|
unread: bool(template.Unread),
|
|
|
|
armBody: template.Body,
|
|
mimeType: template.MIMEType,
|
|
}
|
|
}
|
|
|
|
func (msg *message) toMessage(attData map[string][]byte, att map[string]*attachment) proton.Message {
|
|
return proton.Message{
|
|
MessageMetadata: msg.toMetadata(attData, att),
|
|
|
|
Header: msg.getHeader(),
|
|
ParsedHeaders: msg.getParsedHeaders(),
|
|
Body: msg.armBody,
|
|
MIMEType: msg.mimeType,
|
|
Attachments: xslices.Map(msg.attIDs, func(attID string) proton.Attachment {
|
|
return att[attID].toAttachment()
|
|
}),
|
|
}
|
|
}
|
|
|
|
func (msg *message) getLabelIDs() []string {
|
|
labelIDs := []string{proton.AllMailLabel}
|
|
|
|
if msg.flags.HasAny(proton.MessageFlagSent, proton.MessageFlagScheduledSend) {
|
|
labelIDs = append(labelIDs, proton.AllSentLabel)
|
|
}
|
|
|
|
if !msg.flags.HasAny(proton.MessageFlagSent, proton.MessageFlagScheduledSend, proton.MessageFlagReceived) {
|
|
labelIDs = append(labelIDs, proton.AllDraftsLabel)
|
|
}
|
|
|
|
if msg.starred {
|
|
labelIDs = append(labelIDs, proton.StarredLabel)
|
|
}
|
|
|
|
if msg.sysLabel != nil {
|
|
if *msg.sysLabel != "" {
|
|
labelIDs = append(labelIDs, *msg.sysLabel)
|
|
}
|
|
} else {
|
|
switch {
|
|
case msg.flags.Has(proton.MessageFlagReceived):
|
|
labelIDs = append(labelIDs, proton.InboxLabel)
|
|
|
|
case msg.flags.Has(proton.MessageFlagSent):
|
|
labelIDs = append(labelIDs, proton.SentLabel)
|
|
|
|
case msg.flags.Has(proton.MessageFlagScheduledSend):
|
|
labelIDs = append(labelIDs, proton.AllScheduledLabel)
|
|
|
|
default:
|
|
labelIDs = append(labelIDs, proton.DraftsLabel)
|
|
}
|
|
}
|
|
|
|
return labelIDs
|
|
}
|
|
|
|
func (msg *message) toMetadata(attData map[string][]byte, att map[string]*attachment) proton.MessageMetadata {
|
|
labelIDs := msg.getLabelIDs()
|
|
|
|
messageSize := len(msg.armBody)
|
|
for _, a := range msg.attIDs {
|
|
messageSize += len(attData[att[a].attDataID])
|
|
}
|
|
|
|
return proton.MessageMetadata{
|
|
ID: msg.messageID,
|
|
ExternalID: msg.externalID,
|
|
AddressID: msg.addrID,
|
|
LabelIDs: append(msg.labelIDs, labelIDs...),
|
|
|
|
Subject: msg.subject,
|
|
Sender: msg.sender,
|
|
ToList: msg.toList,
|
|
CCList: msg.ccList,
|
|
BCCList: msg.bccList,
|
|
ReplyTos: msg.replytos,
|
|
Size: messageSize,
|
|
|
|
Flags: msg.flags,
|
|
Unread: proton.Bool(msg.unread),
|
|
|
|
NumAttachments: len(attData),
|
|
}
|
|
}
|
|
|
|
func (msg *message) getHeader() string {
|
|
builder := new(strings.Builder)
|
|
|
|
builder.WriteString("Subject: " + msg.subject + "\r\n")
|
|
|
|
if msg.sender != nil && (msg.sender.Name != "" || msg.sender.Address != "") {
|
|
builder.WriteString("From: " + msg.sender.String() + "\r\n")
|
|
}
|
|
|
|
if len(msg.toList) > 0 {
|
|
builder.WriteString("To: " + toAddressList(msg.toList) + "\r\n")
|
|
}
|
|
|
|
if len(msg.ccList) > 0 {
|
|
builder.WriteString("Cc: " + toAddressList(msg.ccList) + "\r\n")
|
|
}
|
|
|
|
if len(msg.bccList) > 0 {
|
|
builder.WriteString("Bcc: " + toAddressList(msg.bccList) + "\r\n")
|
|
}
|
|
|
|
if msg.mimeType != "" {
|
|
builder.WriteString("Content-Type: " + string(msg.mimeType) + "\r\n")
|
|
}
|
|
|
|
if len(msg.inReplyTo) > 0 {
|
|
builder.WriteString("References: " + msg.inReplyTo + "\r\n")
|
|
}
|
|
|
|
if msg.inReplyTo != "" {
|
|
builder.WriteString("In-Reply-To: " + msg.inReplyTo + "\r\n")
|
|
}
|
|
|
|
builder.WriteString("Date: " + msg.date.Format(time.RFC822) + "\r\n")
|
|
|
|
return builder.String()
|
|
}
|
|
|
|
func (msg *message) getParsedHeaders() proton.Headers {
|
|
header, err := rfc822.NewHeader([]byte(msg.getHeader()))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
parsed := proton.Headers{
|
|
Values: make(map[string][]string),
|
|
}
|
|
|
|
header.Entries(func(key, value string) {
|
|
parsed.Order = append(parsed.Order, key)
|
|
parsed.Values[key] = append(parsed.Values[key], value)
|
|
})
|
|
|
|
return parsed
|
|
}
|
|
|
|
// applyChanges will apply non-nil field from passed message.
|
|
//
|
|
// NOTE: This is not feature complete. It might panic on non-implemented changes.
|
|
func (msg *message) applyChanges(changes proton.DraftTemplate) {
|
|
if changes.Subject != "" {
|
|
msg.subject = changes.Subject
|
|
}
|
|
|
|
if changes.Sender != nil {
|
|
msg.sender = changes.Sender
|
|
}
|
|
|
|
if changes.ToList != nil {
|
|
msg.toList = append([]*mail.Address{}, changes.ToList...)
|
|
}
|
|
|
|
if changes.CCList != nil {
|
|
msg.ccList = append([]*mail.Address{}, changes.CCList...)
|
|
}
|
|
|
|
if changes.BCCList != nil {
|
|
msg.bccList = append([]*mail.Address{}, changes.BCCList...)
|
|
}
|
|
|
|
if changes.Body != "" {
|
|
msg.armBody = changes.Body
|
|
}
|
|
|
|
if changes.MIMEType != "" {
|
|
msg.mimeType = changes.MIMEType
|
|
}
|
|
|
|
if changes.ExternalID != "" {
|
|
msg.externalID = changes.ExternalID
|
|
}
|
|
}
|
|
|
|
func (msg *message) addLabel(labelID string, labels map[string]*label) {
|
|
switch labelID {
|
|
case proton.InboxLabel, proton.SentLabel, proton.DraftsLabel, proton.AllScheduledLabel:
|
|
msg.addFlagLabel(labelID, labels)
|
|
|
|
case proton.TrashLabel, proton.SpamLabel, proton.ArchiveLabel:
|
|
msg.addSystemLabel(labelID, labels)
|
|
|
|
case proton.StarredLabel:
|
|
msg.starred = true
|
|
|
|
default:
|
|
if label, ok := labels[labelID]; ok {
|
|
msg.addUserLabel(label, labels)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (msg *message) addFlagLabel(labelID string, labels map[string]*label) {
|
|
msg.labelIDs = xslices.Filter(msg.labelIDs, func(otherLabelID string) bool {
|
|
return labels[otherLabelID].labelType == proton.LabelTypeLabel
|
|
})
|
|
|
|
msg.sysLabel = nil
|
|
}
|
|
|
|
func (msg *message) addSystemLabel(labelID string, labels map[string]*label) {
|
|
msg.labelIDs = xslices.Filter(msg.labelIDs, func(otherLabelID string) bool {
|
|
return labels[otherLabelID].labelType == proton.LabelTypeLabel
|
|
})
|
|
|
|
msg.sysLabel = &labelID
|
|
}
|
|
|
|
func (msg *message) addUserLabel(label *label, labels map[string]*label) {
|
|
if label.labelType != proton.LabelTypeLabel {
|
|
msg.labelIDs = xslices.Filter(msg.labelIDs, func(otherLabelID string) bool {
|
|
return labels[otherLabelID].labelType == proton.LabelTypeLabel
|
|
})
|
|
|
|
msg.sysLabel = pointer("")
|
|
}
|
|
|
|
if !slices.Contains(msg.labelIDs, label.labelID) {
|
|
msg.labelIDs = append(msg.labelIDs, label.labelID)
|
|
}
|
|
}
|
|
|
|
func (msg *message) remLabel(labelID string, labels map[string]*label) {
|
|
switch labelID {
|
|
case proton.InboxLabel, proton.SentLabel, proton.DraftsLabel, proton.AllScheduledLabel:
|
|
msg.remFlagLabel(labelID, labels)
|
|
|
|
case proton.TrashLabel, proton.SpamLabel, proton.ArchiveLabel:
|
|
msg.remSystemLabel(labelID, labels)
|
|
|
|
case proton.StarredLabel:
|
|
msg.starred = false
|
|
|
|
default:
|
|
if label, ok := labels[labelID]; ok {
|
|
msg.remUserLabel(label, labels)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (msg *message) remFlagLabel(labelID string, labels map[string]*label) {
|
|
if msg.sysLabel == nil {
|
|
msg.sysLabel = pointer("")
|
|
}
|
|
}
|
|
|
|
func (msg *message) remSystemLabel(labelID string, labels map[string]*label) {
|
|
if msg.sysLabel != nil && *msg.sysLabel == labelID {
|
|
msg.sysLabel = pointer("")
|
|
}
|
|
}
|
|
|
|
func (msg *message) remUserLabel(label *label, labels map[string]*label) {
|
|
msg.labelIDs = xslices.Filter(msg.labelIDs, func(otherLabelID string) bool {
|
|
return otherLabelID != label.labelID
|
|
})
|
|
}
|
|
|
|
func toAddressList(addrs []*mail.Address) string {
|
|
res := make([]string, len(addrs))
|
|
|
|
for i, addr := range addrs {
|
|
res[i] = addr.String()
|
|
}
|
|
|
|
return strings.Join(res, ", ")
|
|
}
|
|
|
|
func pointer[T any](v T) *T {
|
|
return &v
|
|
}
|