Merge branch 'addmeto.cc' into 'main'
CC/BCC support See merge request etke.cc/postmoogle!40
This commit is contained in:
@@ -102,6 +102,7 @@ If you want to change them - check available options in the help message (`!pm h
|
|||||||
|
|
||||||
* **!pm nosender** - Get or set `nosender` of the room (`true` - hide email sender; `false` - show email sender)
|
* **!pm nosender** - Get or set `nosender` of the room (`true` - hide email sender; `false` - show email sender)
|
||||||
* **!pm norecipient** - Get or set `norecipient` of the room (`true` - hide recipient; `false` - show recipient)
|
* **!pm norecipient** - Get or set `norecipient` of the room (`true` - hide recipient; `false` - show recipient)
|
||||||
|
* **!pm nocc** - Get or set `nocc` of the room (`true` - hide CC; `false` - show CC)
|
||||||
* **!pm nosubject** - Get or set `nosubject` of the room (`true` - hide email subject; `false` - show email subject)
|
* **!pm nosubject** - Get or set `nosubject` of the room (`true` - hide email subject; `false` - show email subject)
|
||||||
* **!pm nohtml** - Get or set `nohtml` of the room (`true` - ignore HTML in email; `false` - parse HTML in emails)
|
* **!pm nohtml** - Get or set `nohtml` of the room (`true` - ignore HTML in email; `false` - parse HTML in emails)
|
||||||
* **!pm nothreads** - Get or set `nothreads` of the room (`true` - ignore email threads; `false` - convert email threads into matrix threads)
|
* **!pm nothreads** - Get or set `nothreads` of the room (`true` - ignore email threads; `false` - convert email threads into matrix threads)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"maunium.net/go/mautrix/format"
|
"maunium.net/go/mautrix/format"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
|
"gitlab.com/etke.cc/postmoogle/email"
|
||||||
"gitlab.com/etke.cc/postmoogle/utils"
|
"gitlab.com/etke.cc/postmoogle/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -120,6 +121,15 @@ func (b *Bot) initCommands() commandList {
|
|||||||
sanitizer: utils.SanitizeBoolString,
|
sanitizer: utils.SanitizeBoolString,
|
||||||
allowed: b.allowOwner,
|
allowed: b.allowOwner,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: roomOptionNoCC,
|
||||||
|
description: fmt.Sprintf(
|
||||||
|
"Get or set `%s` of the room (`true` - hide CC; `false` - show CC)",
|
||||||
|
roomOptionNoCC,
|
||||||
|
),
|
||||||
|
sanitizer: utils.SanitizeBoolString,
|
||||||
|
allowed: b.allowOwner,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: roomOptionNoSubject,
|
key: roomOptionNoSubject,
|
||||||
description: fmt.Sprintf(
|
description: fmt.Sprintf(
|
||||||
@@ -415,7 +425,7 @@ func (b *Bot) runSend(ctx context.Context) {
|
|||||||
tos := strings.Split(to, ",")
|
tos := strings.Split(to, ",")
|
||||||
// validate first
|
// validate first
|
||||||
for _, to := range tos {
|
for _, to := range tos {
|
||||||
if !utils.AddressValid(to) {
|
if !email.AddressValid(to) {
|
||||||
b.Error(ctx, evt.RoomID, "email address is not valid")
|
b.Error(ctx, evt.RoomID, "email address is not valid")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -426,10 +436,10 @@ func (b *Bot) runSend(ctx context.Context) {
|
|||||||
|
|
||||||
domain := utils.SanitizeDomain(cfg.Domain())
|
domain := utils.SanitizeDomain(cfg.Domain())
|
||||||
from := mailbox + "@" + domain
|
from := mailbox + "@" + domain
|
||||||
ID := utils.MessageID(evt.ID, domain)
|
ID := email.MessageID(evt.ID, domain)
|
||||||
for _, to := range tos {
|
for _, to := range tos {
|
||||||
email := utils.NewEmail(ID, "", " "+ID, subject, from, to, body, htmlBody, nil)
|
eml := email.New(ID, "", " "+ID, subject, from, to, to, "", body, htmlBody, nil)
|
||||||
data := email.Compose(b.getBotSettings().DKIMPrivateKey())
|
data := eml.Compose(b.getBotSettings().DKIMPrivateKey())
|
||||||
if data == "" {
|
if data == "" {
|
||||||
b.SendError(ctx, evt.RoomID, "email body is empty")
|
b.SendError(ctx, evt.RoomID, "email body is empty")
|
||||||
return
|
return
|
||||||
@@ -437,14 +447,14 @@ func (b *Bot) runSend(ctx context.Context) {
|
|||||||
queued, err := b.Sendmail(evt.ID, from, to, data)
|
queued, err := b.Sendmail(evt.ID, from, to, data)
|
||||||
if queued {
|
if queued {
|
||||||
b.log.Error("cannot send email: %v", err)
|
b.log.Error("cannot send email: %v", err)
|
||||||
b.saveSentMetadata(ctx, queued, evt.ID, email, &cfg)
|
b.saveSentMetadata(ctx, queued, evt.ID, eml, &cfg)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot send email to %s: %v", to, err)
|
b.Error(ctx, evt.RoomID, "cannot send email to %s: %v", to, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
b.saveSentMetadata(ctx, false, evt.ID, email, &cfg)
|
b.saveSentMetadata(ctx, false, evt.ID, eml, &cfg)
|
||||||
}
|
}
|
||||||
if len(tos) > 1 {
|
if len(tos) > 1 {
|
||||||
b.SendNotice(ctx, evt.RoomID, "All emails were sent.")
|
b.SendNotice(ctx, evt.RoomID, "All emails were sent.")
|
||||||
|
|||||||
99
bot/email.go
99
bot/email.go
@@ -9,6 +9,7 @@ import (
|
|||||||
"maunium.net/go/mautrix/format"
|
"maunium.net/go/mautrix/format"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
|
"gitlab.com/etke.cc/postmoogle/email"
|
||||||
"gitlab.com/etke.cc/postmoogle/utils"
|
"gitlab.com/etke.cc/postmoogle/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,8 +26,10 @@ const (
|
|||||||
eventReferencesKey = "cc.etke.postmoogle.references"
|
eventReferencesKey = "cc.etke.postmoogle.references"
|
||||||
eventInReplyToKey = "cc.etke.postmoogle.inReplyTo"
|
eventInReplyToKey = "cc.etke.postmoogle.inReplyTo"
|
||||||
eventSubjectKey = "cc.etke.postmoogle.subject"
|
eventSubjectKey = "cc.etke.postmoogle.subject"
|
||||||
|
eventRcptToKey = "cc.etke.postmoogle.rcptTo"
|
||||||
eventFromKey = "cc.etke.postmoogle.from"
|
eventFromKey = "cc.etke.postmoogle.from"
|
||||||
eventToKey = "cc.etke.postmoogle.to"
|
eventToKey = "cc.etke.postmoogle.to"
|
||||||
|
eventCcKey = "cc.etke.postmoogle.cc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetSendmail sets mail sending func to the bot
|
// SetSendmail sets mail sending func to the bot
|
||||||
@@ -83,7 +86,7 @@ func (b *Bot) GetMapping(mailbox string) (id.RoomID, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetIFOptions returns incoming email filtering options (room settings)
|
// GetIFOptions returns incoming email filtering options (room settings)
|
||||||
func (b *Bot) GetIFOptions(roomID id.RoomID) utils.IncomingFilteringOptions {
|
func (b *Bot) GetIFOptions(roomID id.RoomID) email.IncomingFilteringOptions {
|
||||||
cfg, err := b.getRoomSettings(roomID)
|
cfg, err := b.getRoomSettings(roomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.log.Error("cannot retrieve room settings: %v", err)
|
b.log.Error("cannot retrieve room settings: %v", err)
|
||||||
@@ -94,7 +97,7 @@ func (b *Bot) GetIFOptions(roomID id.RoomID) utils.IncomingFilteringOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IncomingEmail sends incoming email to matrix room
|
// IncomingEmail sends incoming email to matrix room
|
||||||
func (b *Bot) IncomingEmail(ctx context.Context, email *utils.Email) error {
|
func (b *Bot) IncomingEmail(ctx context.Context, email *email.Email) error {
|
||||||
roomID, ok := b.GetMapping(email.Mailbox(true))
|
roomID, ok := b.GetMapping(email.Mailbox(true))
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("room not found")
|
return errors.New("room not found")
|
||||||
@@ -147,18 +150,11 @@ func (b *Bot) SendEmailReply(ctx context.Context) {
|
|||||||
b.Error(ctx, evt.RoomID, "mailbox is not configured, kupo")
|
b.Error(ctx, evt.RoomID, "mailbox is not configured, kupo")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
domain := utils.SanitizeDomain(cfg.Domain())
|
|
||||||
|
|
||||||
b.lock(evt.RoomID.String())
|
b.lock(evt.RoomID.String())
|
||||||
defer b.unlock(evt.RoomID.String())
|
defer b.unlock(evt.RoomID.String())
|
||||||
|
|
||||||
fromMailbox := mailbox + "@" + domain
|
meta := b.getParentEmail(evt, mailbox)
|
||||||
meta := b.getParentEmail(evt, domain)
|
|
||||||
// when email was sent from matrix and reply was sent from matrix again
|
|
||||||
if fromMailbox != meta.From {
|
|
||||||
meta.To = meta.From
|
|
||||||
}
|
|
||||||
meta.From = fromMailbox
|
|
||||||
|
|
||||||
if meta.To == "" {
|
if meta.To == "" {
|
||||||
b.Error(ctx, evt.RoomID, "cannot find parent email and continue the thread. Please, start a new email thread")
|
b.Error(ctx, evt.RoomID, "cannot find parent email and continue the thread. Please, start a new email thread")
|
||||||
@@ -175,11 +171,11 @@ func (b *Bot) SendEmailReply(ctx context.Context) {
|
|||||||
body := content.Body
|
body := content.Body
|
||||||
htmlBody := content.FormattedBody
|
htmlBody := content.FormattedBody
|
||||||
|
|
||||||
meta.MessageID = utils.MessageID(evt.ID, domain)
|
meta.MessageID = email.MessageID(evt.ID, meta.FromDomain)
|
||||||
meta.References = meta.References + " " + meta.MessageID
|
meta.References = meta.References + " " + meta.MessageID
|
||||||
b.log.Debug("send email reply: %+v", meta)
|
b.log.Debug("send email reply: %+v", meta)
|
||||||
email := utils.NewEmail(meta.MessageID, meta.InReplyTo, meta.References, meta.Subject, meta.From, meta.To, body, htmlBody, nil)
|
eml := email.New(meta.MessageID, meta.InReplyTo, meta.References, meta.Subject, meta.From, meta.To, meta.RcptTo, meta.CC, body, htmlBody, nil)
|
||||||
data := email.Compose(b.getBotSettings().DKIMPrivateKey())
|
data := eml.Compose(b.getBotSettings().DKIMPrivateKey())
|
||||||
if data == "" {
|
if data == "" {
|
||||||
b.SendError(ctx, evt.RoomID, "email body is empty")
|
b.SendError(ctx, evt.RoomID, "email body is empty")
|
||||||
return
|
return
|
||||||
@@ -188,7 +184,7 @@ func (b *Bot) SendEmailReply(ctx context.Context) {
|
|||||||
queued, err := b.Sendmail(evt.ID, meta.From, meta.To, data)
|
queued, err := b.Sendmail(evt.ID, meta.From, meta.To, data)
|
||||||
if queued {
|
if queued {
|
||||||
b.log.Error("cannot send email: %v", err)
|
b.log.Error("cannot send email: %v", err)
|
||||||
b.saveSentMetadata(ctx, queued, meta.ThreadID, email, &cfg)
|
b.saveSentMetadata(ctx, queued, meta.ThreadID, eml, &cfg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,19 +193,71 @@ func (b *Bot) SendEmailReply(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b.saveSentMetadata(ctx, queued, meta.ThreadID, email, &cfg)
|
b.saveSentMetadata(ctx, queued, meta.ThreadID, eml, &cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
type parentEmail struct {
|
type parentEmail struct {
|
||||||
MessageID string
|
MessageID string
|
||||||
ThreadID id.EventID
|
ThreadID id.EventID
|
||||||
From string
|
From string
|
||||||
|
FromDomain string
|
||||||
To string
|
To string
|
||||||
|
RcptTo string
|
||||||
|
CC string
|
||||||
InReplyTo string
|
InReplyTo string
|
||||||
References string
|
References string
|
||||||
Subject string
|
Subject string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fixtofrom attempts to "fix" or rather reverse the To, From and CC headers
|
||||||
|
// of parent email by using parent email as metadata source for a new email
|
||||||
|
// that will be sent from postmoogle.
|
||||||
|
// To do so, we need to reverse From and To headers, but Cc should be adjusted as well,
|
||||||
|
// thus that hacky workaround below:
|
||||||
|
func (e *parentEmail) fixtofrom(newSenderMailbox string, domains []string) {
|
||||||
|
newSenders := make(map[string]string, len(domains))
|
||||||
|
for _, domain := range domains {
|
||||||
|
sender := newSenderMailbox + "@" + domain
|
||||||
|
newSenders[sender] = sender
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to determine previous email of the room mailbox
|
||||||
|
// by matching RCPT TO, To and From fields
|
||||||
|
// why? Because of possible multi-domain setup and we won't leak information
|
||||||
|
var previousSender string
|
||||||
|
rcptToSender, ok := newSenders[e.RcptTo]
|
||||||
|
if ok {
|
||||||
|
previousSender = rcptToSender
|
||||||
|
}
|
||||||
|
toSender, ok := newSenders[e.To]
|
||||||
|
if ok {
|
||||||
|
previousSender = toSender
|
||||||
|
}
|
||||||
|
fromSender, ok := newSenders[e.From]
|
||||||
|
if ok {
|
||||||
|
previousSender = fromSender
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message-Id should not leak information either
|
||||||
|
e.FromDomain = utils.SanitizeDomain(utils.Hostname(previousSender))
|
||||||
|
|
||||||
|
originalFrom := e.From
|
||||||
|
// reverse From if needed
|
||||||
|
if fromSender == "" {
|
||||||
|
e.From = previousSender
|
||||||
|
}
|
||||||
|
// reverse To if needed
|
||||||
|
if toSender != "" {
|
||||||
|
e.To = originalFrom
|
||||||
|
}
|
||||||
|
// replace previous recipient of the email which is sender now with the original From
|
||||||
|
for newSender := range newSenders {
|
||||||
|
if strings.Contains(e.CC, newSender) {
|
||||||
|
e.CC = strings.ReplaceAll(e.CC, newSender, originalFrom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Bot) getParentEvent(evt *event.Event) (id.EventID, *event.Event) {
|
func (b *Bot) getParentEvent(evt *event.Event) (id.EventID, *event.Event) {
|
||||||
content := evt.Content.AsMessage()
|
content := evt.Content.AsMessage()
|
||||||
threadID := utils.EventParent(evt.ID, content)
|
threadID := utils.EventParent(evt.ID, content)
|
||||||
@@ -246,8 +294,8 @@ func (b *Bot) getParentEvent(evt *event.Event) (id.EventID, *event.Event) {
|
|||||||
return threadID, decrypted
|
return threadID, decrypted
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) getParentEmail(evt *event.Event, domain string) parentEmail {
|
func (b *Bot) getParentEmail(evt *event.Event, newFromMailbox string) *parentEmail {
|
||||||
var parent parentEmail
|
parent := &parentEmail{}
|
||||||
threadID, parentEvt := b.getParentEvent(evt)
|
threadID, parentEvt := b.getParentEvent(evt)
|
||||||
parent.ThreadID = threadID
|
parent.ThreadID = threadID
|
||||||
if parentEvt == nil {
|
if parentEvt == nil {
|
||||||
@@ -257,11 +305,14 @@ func (b *Bot) getParentEmail(evt *event.Event, domain string) parentEmail {
|
|||||||
return parent
|
return parent
|
||||||
}
|
}
|
||||||
|
|
||||||
parent.MessageID = utils.MessageID(parentEvt.ID, domain)
|
|
||||||
parent.From = utils.EventField[string](&parentEvt.Content, eventFromKey)
|
parent.From = utils.EventField[string](&parentEvt.Content, eventFromKey)
|
||||||
parent.To = utils.EventField[string](&parentEvt.Content, eventToKey)
|
parent.To = utils.EventField[string](&parentEvt.Content, eventToKey)
|
||||||
|
parent.CC = utils.EventField[string](&parentEvt.Content, eventCcKey)
|
||||||
|
parent.RcptTo = utils.EventField[string](&parentEvt.Content, eventRcptToKey)
|
||||||
parent.InReplyTo = utils.EventField[string](&parentEvt.Content, eventMessageIDkey)
|
parent.InReplyTo = utils.EventField[string](&parentEvt.Content, eventMessageIDkey)
|
||||||
parent.References = utils.EventField[string](&parentEvt.Content, eventReferencesKey)
|
parent.References = utils.EventField[string](&parentEvt.Content, eventReferencesKey)
|
||||||
|
parent.fixtofrom(newFromMailbox, b.domains)
|
||||||
|
parent.MessageID = email.MessageID(parentEvt.ID, parent.FromDomain)
|
||||||
if parent.InReplyTo == "" {
|
if parent.InReplyTo == "" {
|
||||||
parent.InReplyTo = parent.MessageID
|
parent.InReplyTo = parent.MessageID
|
||||||
}
|
}
|
||||||
@@ -281,14 +332,14 @@ func (b *Bot) getParentEmail(evt *event.Event, domain string) parentEmail {
|
|||||||
|
|
||||||
// saveSentMetadata used to save metadata from !pm sent and thread reply events to a separate notice message
|
// saveSentMetadata used to save metadata from !pm sent and thread reply events to a separate notice message
|
||||||
// because that metadata is needed to determine email thread relations
|
// because that metadata is needed to determine email thread relations
|
||||||
func (b *Bot) saveSentMetadata(ctx context.Context, queued bool, threadID id.EventID, email *utils.Email, cfg *roomSettings) {
|
func (b *Bot) saveSentMetadata(ctx context.Context, queued bool, threadID id.EventID, eml *email.Email, cfg *roomSettings) {
|
||||||
text := "Email has been sent to " + email.To
|
text := "Email has been sent to " + eml.RcptTo
|
||||||
if queued {
|
if queued {
|
||||||
text = "Email to " + email.To + " has been queued"
|
text = "Email to " + eml.RcptTo + " has been queued"
|
||||||
}
|
}
|
||||||
|
|
||||||
evt := eventFromContext(ctx)
|
evt := eventFromContext(ctx)
|
||||||
content := email.Content(threadID, cfg.ContentOptions())
|
content := eml.Content(threadID, cfg.ContentOptions())
|
||||||
notice := format.RenderMarkdown(text, true, true)
|
notice := format.RenderMarkdown(text, true, true)
|
||||||
msgContent, ok := content.Parsed.(*event.MessageEventContent)
|
msgContent, ok := content.Parsed.(*event.MessageEventContent)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -305,8 +356,8 @@ func (b *Bot) saveSentMetadata(ctx context.Context, queued bool, threadID id.Eve
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
domain := utils.SanitizeDomain(cfg.Domain())
|
domain := utils.SanitizeDomain(cfg.Domain())
|
||||||
b.setThreadID(evt.RoomID, utils.MessageID(evt.ID, domain), threadID)
|
b.setThreadID(evt.RoomID, email.MessageID(evt.ID, domain), threadID)
|
||||||
b.setThreadID(evt.RoomID, utils.MessageID(msgID, domain), threadID)
|
b.setThreadID(evt.RoomID, email.MessageID(msgID, domain), threadID)
|
||||||
b.setLastEventID(evt.RoomID, threadID, msgID)
|
b.setLastEventID(evt.RoomID, threadID, msgID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
|
"gitlab.com/etke.cc/postmoogle/email"
|
||||||
"gitlab.com/etke.cc/postmoogle/utils"
|
"gitlab.com/etke.cc/postmoogle/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ const (
|
|||||||
roomOptionMailbox = "mailbox"
|
roomOptionMailbox = "mailbox"
|
||||||
roomOptionDomain = "domain"
|
roomOptionDomain = "domain"
|
||||||
roomOptionNoSend = "nosend"
|
roomOptionNoSend = "nosend"
|
||||||
|
roomOptionNoCC = "nocc"
|
||||||
roomOptionNoSender = "nosender"
|
roomOptionNoSender = "nosender"
|
||||||
roomOptionNoRecipient = "norecipient"
|
roomOptionNoRecipient = "norecipient"
|
||||||
roomOptionNoSubject = "nosubject"
|
roomOptionNoSubject = "nosubject"
|
||||||
@@ -61,6 +63,10 @@ func (s roomSettings) NoSend() bool {
|
|||||||
return utils.Bool(s.Get(roomOptionNoSend))
|
return utils.Bool(s.Get(roomOptionNoSend))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s roomSettings) NoCC() bool {
|
||||||
|
return utils.Bool(s.Get(roomOptionNoCC))
|
||||||
|
}
|
||||||
|
|
||||||
func (s roomSettings) NoSender() bool {
|
func (s roomSettings) NoSender() bool {
|
||||||
return utils.Bool(s.Get(roomOptionNoSender))
|
return utils.Bool(s.Get(roomOptionNoSender))
|
||||||
}
|
}
|
||||||
@@ -143,8 +149,9 @@ func (s roomSettings) migrateSpamlistSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ContentOptions converts room display settings to content options
|
// ContentOptions converts room display settings to content options
|
||||||
func (s roomSettings) ContentOptions() *utils.ContentOptions {
|
func (s roomSettings) ContentOptions() *email.ContentOptions {
|
||||||
return &utils.ContentOptions{
|
return &email.ContentOptions{
|
||||||
|
CC: !s.NoCC(),
|
||||||
HTML: !s.NoHTML(),
|
HTML: !s.NoHTML(),
|
||||||
Sender: !s.NoSender(),
|
Sender: !s.NoSender(),
|
||||||
Recipient: !s.NoRecipient(),
|
Recipient: !s.NoRecipient(),
|
||||||
@@ -152,7 +159,9 @@ func (s roomSettings) ContentOptions() *utils.ContentOptions {
|
|||||||
Threads: !s.NoThreads(),
|
Threads: !s.NoThreads(),
|
||||||
|
|
||||||
ToKey: eventToKey,
|
ToKey: eventToKey,
|
||||||
|
CcKey: eventCcKey,
|
||||||
FromKey: eventFromKey,
|
FromKey: eventFromKey,
|
||||||
|
RcptToKey: eventRcptToKey,
|
||||||
SubjectKey: eventSubjectKey,
|
SubjectKey: eventSubjectKey,
|
||||||
MessageIDKey: eventMessageIDkey,
|
MessageIDKey: eventMessageIDkey,
|
||||||
InReplyToKey: eventInReplyToKey,
|
InReplyToKey: eventInReplyToKey,
|
||||||
|
|||||||
@@ -1,31 +1,20 @@
|
|||||||
package utils
|
package email
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
|
||||||
"net/mail"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/emersion/go-msgauth/dkim"
|
"github.com/emersion/go-msgauth/dkim"
|
||||||
"github.com/jhillyerd/enmime"
|
"github.com/jhillyerd/enmime"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/format"
|
"maunium.net/go/mautrix/format"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
|
"gitlab.com/etke.cc/postmoogle/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var styleRegex = regexp.MustCompile("<style((.|\n|\r)*?)<\\/style>")
|
|
||||||
|
|
||||||
// IncomingFilteringOptions for incoming mail
|
|
||||||
type IncomingFilteringOptions interface {
|
|
||||||
SpamcheckSMTP() bool
|
|
||||||
SpamcheckMX() bool
|
|
||||||
Spamlist() []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Email object
|
// Email object
|
||||||
type Email struct {
|
type Email struct {
|
||||||
Date string
|
Date string
|
||||||
@@ -34,50 +23,25 @@ type Email struct {
|
|||||||
References string
|
References string
|
||||||
From string
|
From string
|
||||||
To string
|
To string
|
||||||
|
RcptTo string
|
||||||
|
CC string
|
||||||
Subject string
|
Subject string
|
||||||
Text string
|
Text string
|
||||||
HTML string
|
HTML string
|
||||||
Files []*File
|
Files []*utils.File
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContentOptions represents settings that specify how an email is to be converted to a Matrix message
|
// New constructs Email object
|
||||||
type ContentOptions struct {
|
func New(messageID, inReplyTo, references, subject, from, to, rcptto, cc, text, html string, files []*utils.File) *Email {
|
||||||
// On/Off
|
|
||||||
Sender bool
|
|
||||||
Recipient bool
|
|
||||||
Subject bool
|
|
||||||
HTML bool
|
|
||||||
Threads bool
|
|
||||||
|
|
||||||
// Keys
|
|
||||||
MessageIDKey string
|
|
||||||
InReplyToKey string
|
|
||||||
ReferencesKey string
|
|
||||||
SubjectKey string
|
|
||||||
FromKey string
|
|
||||||
ToKey string
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddressValid checks if email address is valid
|
|
||||||
func AddressValid(email string) bool {
|
|
||||||
_, err := mail.ParseAddress(email)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessageID generates email Message-Id from matrix event ID
|
|
||||||
func MessageID(eventID id.EventID, domain string) string {
|
|
||||||
return fmt.Sprintf("<%s@%s>", eventID, domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEmail constructs Email object
|
|
||||||
func NewEmail(messageID, inReplyTo, references, subject, from, to, text, html string, files []*File) *Email {
|
|
||||||
email := &Email{
|
email := &Email{
|
||||||
Date: time.Now().UTC().Format(time.RFC1123Z),
|
Date: dateNow(),
|
||||||
MessageID: messageID,
|
MessageID: messageID,
|
||||||
InReplyTo: inReplyTo,
|
InReplyTo: inReplyTo,
|
||||||
References: references,
|
References: references,
|
||||||
From: from,
|
From: from,
|
||||||
To: to,
|
To: to,
|
||||||
|
CC: cc,
|
||||||
|
RcptTo: rcptto,
|
||||||
Subject: subject,
|
Subject: subject,
|
||||||
Text: text,
|
Text: text,
|
||||||
HTML: html,
|
HTML: html,
|
||||||
@@ -92,12 +56,46 @@ func NewEmail(messageID, inReplyTo, references, subject, from, to, text, html st
|
|||||||
return email
|
return email
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FromEnvelope constructs Email object from envelope
|
||||||
|
func FromEnvelope(rcptto string, envelope *enmime.Envelope) *Email {
|
||||||
|
datetime, _ := envelope.Date() //nolint:errcheck // handled in dateNow()
|
||||||
|
date := dateNow(datetime)
|
||||||
|
|
||||||
|
var html string
|
||||||
|
if envelope.HTML != "" {
|
||||||
|
html = styleRegex.ReplaceAllString(envelope.HTML, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
files := make([]*utils.File, 0, len(envelope.Attachments))
|
||||||
|
for _, attachment := range envelope.Attachments {
|
||||||
|
file := utils.NewFile(attachment.FileName, attachment.Content)
|
||||||
|
files = append(files, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
email := &Email{
|
||||||
|
Date: date,
|
||||||
|
MessageID: envelope.GetHeader("Message-Id"),
|
||||||
|
InReplyTo: envelope.GetHeader("In-Reply-To"),
|
||||||
|
References: envelope.GetHeader("References"),
|
||||||
|
From: envelope.GetHeader("From"),
|
||||||
|
To: envelope.GetHeader("To"),
|
||||||
|
RcptTo: rcptto,
|
||||||
|
CC: envelope.GetHeader("Cc"),
|
||||||
|
Subject: envelope.GetHeader("Subject"),
|
||||||
|
Text: envelope.Text,
|
||||||
|
HTML: html,
|
||||||
|
Files: files,
|
||||||
|
}
|
||||||
|
|
||||||
|
return email
|
||||||
|
}
|
||||||
|
|
||||||
// Mailbox returns postmoogle's mailbox, parsing it from FROM (if incoming=false) or TO (incoming=true)
|
// Mailbox returns postmoogle's mailbox, parsing it from FROM (if incoming=false) or TO (incoming=true)
|
||||||
func (e *Email) Mailbox(incoming bool) string {
|
func (e *Email) Mailbox(incoming bool) string {
|
||||||
if incoming {
|
if incoming {
|
||||||
return Mailbox(e.To)
|
return utils.Mailbox(e.RcptTo)
|
||||||
}
|
}
|
||||||
return Mailbox(e.From)
|
return utils.Mailbox(e.From)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content converts the email object to a Matrix event content
|
// Content converts the email object to a Matrix event content
|
||||||
@@ -110,7 +108,11 @@ func (e *Email) Content(threadID id.EventID, options *ContentOptions) *event.Con
|
|||||||
text.WriteString(" ➡️ ")
|
text.WriteString(" ➡️ ")
|
||||||
text.WriteString(e.To)
|
text.WriteString(e.To)
|
||||||
}
|
}
|
||||||
if options.Sender || options.Recipient {
|
if options.CC && e.CC != "" {
|
||||||
|
text.WriteString("\ncc: ")
|
||||||
|
text.WriteString(e.CC)
|
||||||
|
}
|
||||||
|
if options.Sender || options.Recipient || options.CC {
|
||||||
text.WriteString("\n\n")
|
text.WriteString("\n\n")
|
||||||
}
|
}
|
||||||
if options.Subject && threadID == "" {
|
if options.Subject && threadID == "" {
|
||||||
@@ -125,7 +127,7 @@ func (e *Email) Content(threadID id.EventID, options *ContentOptions) *event.Con
|
|||||||
}
|
}
|
||||||
|
|
||||||
parsed := format.RenderMarkdown(text.String(), true, true)
|
parsed := format.RenderMarkdown(text.String(), true, true)
|
||||||
parsed.RelatesTo = RelatesTo(options.Threads, threadID)
|
parsed.RelatesTo = utils.RelatesTo(options.Threads, threadID)
|
||||||
|
|
||||||
content := event.Content{
|
content := event.Content{
|
||||||
Raw: map[string]interface{}{
|
Raw: map[string]interface{}{
|
||||||
@@ -133,8 +135,10 @@ func (e *Email) Content(threadID id.EventID, options *ContentOptions) *event.Con
|
|||||||
options.InReplyToKey: e.InReplyTo,
|
options.InReplyToKey: e.InReplyTo,
|
||||||
options.ReferencesKey: e.References,
|
options.ReferencesKey: e.References,
|
||||||
options.SubjectKey: e.Subject,
|
options.SubjectKey: e.Subject,
|
||||||
|
options.RcptToKey: e.RcptTo,
|
||||||
options.FromKey: e.From,
|
options.FromKey: e.From,
|
||||||
options.ToKey: e.To,
|
options.ToKey: e.To,
|
||||||
|
options.CcKey: e.CC,
|
||||||
},
|
},
|
||||||
Parsed: &parsed,
|
Parsed: &parsed,
|
||||||
}
|
}
|
||||||
@@ -169,13 +173,11 @@ func (e *Email) Compose(privkey string) string {
|
|||||||
|
|
||||||
root, err := mail.Build()
|
root, err := mail.Build()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("cannot compose email: %v", err)
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
var data strings.Builder
|
var data strings.Builder
|
||||||
err = root.Encode(&data)
|
err = root.Encode(&data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("cannot encode email: %v", err)
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
29
email/options.go
Normal file
29
email/options.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package email
|
||||||
|
|
||||||
|
// IncomingFilteringOptions for incoming mail
|
||||||
|
type IncomingFilteringOptions interface {
|
||||||
|
SpamcheckSMTP() bool
|
||||||
|
SpamcheckMX() bool
|
||||||
|
Spamlist() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContentOptions represents settings that specify how an email is to be converted to a Matrix message
|
||||||
|
type ContentOptions struct {
|
||||||
|
// On/Off
|
||||||
|
CC bool
|
||||||
|
Sender bool
|
||||||
|
Recipient bool
|
||||||
|
Subject bool
|
||||||
|
HTML bool
|
||||||
|
Threads bool
|
||||||
|
|
||||||
|
// Keys
|
||||||
|
MessageIDKey string
|
||||||
|
InReplyToKey string
|
||||||
|
ReferencesKey string
|
||||||
|
SubjectKey string
|
||||||
|
FromKey string
|
||||||
|
ToKey string
|
||||||
|
CcKey string
|
||||||
|
RcptToKey string
|
||||||
|
}
|
||||||
33
email/utils.go
Normal file
33
email/utils.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package email
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/mail"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
var styleRegex = regexp.MustCompile("<style((.|\n|\r)*?)<\\/style>")
|
||||||
|
|
||||||
|
// AddressValid checks if email address is valid
|
||||||
|
func AddressValid(email string) bool {
|
||||||
|
_, err := mail.ParseAddress(email)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageID generates email Message-Id from matrix event ID
|
||||||
|
func MessageID(eventID id.EventID, domain string) string {
|
||||||
|
return fmt.Sprintf("<%s@%s>", eventID, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dateNow returns Date in RFC1123 with numeric timezone
|
||||||
|
func dateNow(original ...time.Time) string {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
if len(original) > 0 && !original[0].IsZero() {
|
||||||
|
now = original[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return now.Format(time.RFC1123Z)
|
||||||
|
}
|
||||||
2
go.mod
2
go.mod
@@ -22,7 +22,6 @@ require (
|
|||||||
gitlab.com/etke.cc/go/trysmtp v1.0.0
|
gitlab.com/etke.cc/go/trysmtp v1.0.0
|
||||||
gitlab.com/etke.cc/go/validator v1.0.4
|
gitlab.com/etke.cc/go/validator v1.0.4
|
||||||
gitlab.com/etke.cc/linkpearl v0.0.0-20221116205701-65547c5608e6
|
gitlab.com/etke.cc/linkpearl v0.0.0-20221116205701-65547c5608e6
|
||||||
golang.org/x/net v0.2.0
|
|
||||||
maunium.net/go/mautrix v0.12.3
|
maunium.net/go/mautrix v0.12.3
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,6 +48,7 @@ require (
|
|||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
github.com/yuin/goldmark v1.5.3 // indirect
|
github.com/yuin/goldmark v1.5.3 // indirect
|
||||||
golang.org/x/crypto v0.3.0 // indirect
|
golang.org/x/crypto v0.3.0 // indirect
|
||||||
|
golang.org/x/net v0.2.0 // indirect
|
||||||
golang.org/x/sys v0.2.0 // indirect
|
golang.org/x/sys v0.2.0 // indirect
|
||||||
golang.org/x/text v0.4.0 // indirect
|
golang.org/x/text v0.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"gitlab.com/etke.cc/go/logger"
|
"gitlab.com/etke.cc/go/logger"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
"gitlab.com/etke.cc/postmoogle/utils"
|
"gitlab.com/etke.cc/postmoogle/email"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -45,8 +45,8 @@ type matrixbot interface {
|
|||||||
IsBanned(net.Addr) bool
|
IsBanned(net.Addr) bool
|
||||||
Ban(net.Addr)
|
Ban(net.Addr)
|
||||||
GetMapping(string) (id.RoomID, bool)
|
GetMapping(string) (id.RoomID, bool)
|
||||||
GetIFOptions(id.RoomID) utils.IncomingFilteringOptions
|
GetIFOptions(id.RoomID) email.IncomingFilteringOptions
|
||||||
IncomingEmail(context.Context, *utils.Email) error
|
IncomingEmail(context.Context, *email.Email) error
|
||||||
SetSendmail(func(string, string, string) error)
|
SetSendmail(func(string, string, string) error)
|
||||||
GetDKIMprivkey() string
|
GetDKIMprivkey() string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"gitlab.com/etke.cc/go/logger"
|
"gitlab.com/etke.cc/go/logger"
|
||||||
"gitlab.com/etke.cc/go/trysmtp"
|
"gitlab.com/etke.cc/go/trysmtp"
|
||||||
|
|
||||||
"gitlab.com/etke.cc/postmoogle/utils"
|
"gitlab.com/etke.cc/postmoogle/email"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -41,7 +41,7 @@ func (m *mailServer) Login(state *smtp.ConnectionState, username, password strin
|
|||||||
return nil, ErrBanned
|
return nil, ErrBanned
|
||||||
}
|
}
|
||||||
|
|
||||||
if !utils.AddressValid(username) {
|
if !email.AddressValid(username) {
|
||||||
m.log.Debug("address %s is invalid", username)
|
m.log.Debug("address %s is invalid", username)
|
||||||
m.bot.Ban(state.RemoteAddr)
|
m.bot.Ban(state.RemoteAddr)
|
||||||
return nil, ErrBanned
|
return nil, ErrBanned
|
||||||
@@ -60,6 +60,7 @@ func (m *mailServer) Login(state *smtp.ConnectionState, username, password strin
|
|||||||
from: username,
|
from: username,
|
||||||
log: m.log,
|
log: m.log,
|
||||||
domains: m.domains,
|
domains: m.domains,
|
||||||
|
tos: []string{},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +81,7 @@ func (m *mailServer) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session,
|
|||||||
log: m.log,
|
log: m.log,
|
||||||
domains: m.domains,
|
domains: m.domains,
|
||||||
addr: state.RemoteAddr,
|
addr: state.RemoteAddr,
|
||||||
|
tos: []string{},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +114,6 @@ func (m *mailServer) SendEmail(from, to, data string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ReceiveEmail - incoming mail into matrix room
|
// ReceiveEmail - incoming mail into matrix room
|
||||||
func (m *mailServer) ReceiveEmail(ctx context.Context, email *utils.Email) error {
|
func (m *mailServer) ReceiveEmail(ctx context.Context, eml *email.Email) error {
|
||||||
return m.bot.IncomingEmail(ctx, email)
|
return m.bot.IncomingEmail(ctx, eml)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"gitlab.com/etke.cc/go/validator"
|
"gitlab.com/etke.cc/go/validator"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
|
"gitlab.com/etke.cc/postmoogle/email"
|
||||||
"gitlab.com/etke.cc/postmoogle/utils"
|
"gitlab.com/etke.cc/postmoogle/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,21 +21,21 @@ import (
|
|||||||
type incomingSession struct {
|
type incomingSession struct {
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
getRoomID func(string) (id.RoomID, bool)
|
getRoomID func(string) (id.RoomID, bool)
|
||||||
getFilters func(id.RoomID) utils.IncomingFilteringOptions
|
getFilters func(id.RoomID) email.IncomingFilteringOptions
|
||||||
receiveEmail func(context.Context, *utils.Email) error
|
receiveEmail func(context.Context, *email.Email) error
|
||||||
greylisted func(net.Addr) bool
|
greylisted func(net.Addr) bool
|
||||||
ban func(net.Addr)
|
ban func(net.Addr)
|
||||||
domains []string
|
domains []string
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
addr net.Addr
|
addr net.Addr
|
||||||
to string
|
tos []string
|
||||||
from string
|
from string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *incomingSession) Mail(from string, opts smtp.MailOptions) error {
|
func (s *incomingSession) Mail(from string, opts smtp.MailOptions) error {
|
||||||
sentry.GetHubFromContext(s.ctx).Scope().SetTag("from", from)
|
sentry.GetHubFromContext(s.ctx).Scope().SetTag("from", from)
|
||||||
if !utils.AddressValid(from) {
|
if !email.AddressValid(from) {
|
||||||
s.log.Debug("address %s is invalid", from)
|
s.log.Debug("address %s is invalid", from)
|
||||||
s.ban(s.addr)
|
s.ban(s.addr)
|
||||||
return ErrBanned
|
return ErrBanned
|
||||||
@@ -46,7 +47,7 @@ func (s *incomingSession) Mail(from string, opts smtp.MailOptions) error {
|
|||||||
|
|
||||||
func (s *incomingSession) Rcpt(to string) error {
|
func (s *incomingSession) Rcpt(to string) error {
|
||||||
sentry.GetHubFromContext(s.ctx).Scope().SetTag("to", to)
|
sentry.GetHubFromContext(s.ctx).Scope().SetTag("to", to)
|
||||||
s.to = to
|
s.tos = append(s.tos, to)
|
||||||
var domainok bool
|
var domainok bool
|
||||||
for _, domain := range s.domains {
|
for _, domain := range s.domains {
|
||||||
if utils.Hostname(to) == domain {
|
if utils.Hostname(to) == domain {
|
||||||
@@ -66,7 +67,7 @@ func (s *incomingSession) Rcpt(to string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validations := s.getFilters(roomID)
|
validations := s.getFilters(roomID)
|
||||||
if !validateEmail(s.from, s.to, s.log, validations) {
|
if !validateEmail(s.from, to, s.log, validations) {
|
||||||
s.ban(s.addr)
|
s.ban(s.addr)
|
||||||
return ErrBanned
|
return ErrBanned
|
||||||
}
|
}
|
||||||
@@ -84,25 +85,20 @@ func (s *incomingSession) Data(r io.Reader) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
parser := enmime.NewParser()
|
parser := enmime.NewParser()
|
||||||
eml, err := parser.ReadEnvelope(r)
|
envelope, err := parser.ReadEnvelope(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
files := parseAttachments(eml.Attachments, s.log)
|
eml := email.FromEnvelope(s.tos[0], envelope)
|
||||||
|
for _, to := range s.tos {
|
||||||
email := utils.NewEmail(
|
eml.RcptTo = to
|
||||||
eml.GetHeader("Message-Id"),
|
err := s.receiveEmail(s.ctx, eml)
|
||||||
eml.GetHeader("In-Reply-To"),
|
if err != nil {
|
||||||
eml.GetHeader("References"),
|
return err
|
||||||
eml.GetHeader("Subject"),
|
}
|
||||||
s.from,
|
}
|
||||||
s.to,
|
return nil
|
||||||
eml.Text,
|
|
||||||
eml.HTML,
|
|
||||||
files)
|
|
||||||
|
|
||||||
return s.receiveEmail(s.ctx, email)
|
|
||||||
}
|
}
|
||||||
func (s *incomingSession) Reset() {}
|
func (s *incomingSession) Reset() {}
|
||||||
func (s *incomingSession) Logout() error { return nil }
|
func (s *incomingSession) Logout() error { return nil }
|
||||||
@@ -115,13 +111,13 @@ type outgoingSession struct {
|
|||||||
domains []string
|
domains []string
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
to string
|
tos []string
|
||||||
from string
|
from string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *outgoingSession) Mail(from string, opts smtp.MailOptions) error {
|
func (s *outgoingSession) Mail(from string, opts smtp.MailOptions) error {
|
||||||
sentry.GetHubFromContext(s.ctx).Scope().SetTag("from", from)
|
sentry.GetHubFromContext(s.ctx).Scope().SetTag("from", from)
|
||||||
if !utils.AddressValid(from) {
|
if !email.AddressValid(from) {
|
||||||
return errors.New("please, provide email address")
|
return errors.New("please, provide email address")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -129,7 +125,7 @@ func (s *outgoingSession) Mail(from string, opts smtp.MailOptions) error {
|
|||||||
|
|
||||||
func (s *outgoingSession) Rcpt(to string) error {
|
func (s *outgoingSession) Rcpt(to string) error {
|
||||||
sentry.GetHubFromContext(s.ctx).Scope().SetTag("to", to)
|
sentry.GetHubFromContext(s.ctx).Scope().SetTag("to", to)
|
||||||
s.to = to
|
s.tos = append(s.tos, to)
|
||||||
|
|
||||||
s.log.Debug("mail to %s", to)
|
s.log.Debug("mail to %s", to)
|
||||||
return nil
|
return nil
|
||||||
@@ -137,30 +133,25 @@ func (s *outgoingSession) Rcpt(to string) error {
|
|||||||
|
|
||||||
func (s *outgoingSession) Data(r io.Reader) error {
|
func (s *outgoingSession) Data(r io.Reader) error {
|
||||||
parser := enmime.NewParser()
|
parser := enmime.NewParser()
|
||||||
eml, err := parser.ReadEnvelope(r)
|
envelope, err := parser.ReadEnvelope(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
eml := email.FromEnvelope(s.tos[0], envelope)
|
||||||
|
for _, to := range s.tos {
|
||||||
|
eml.RcptTo = to
|
||||||
|
err := s.sendmail(eml.From, to, eml.Compose(s.privkey))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
files := parseAttachments(eml.Attachments, s.log)
|
return nil
|
||||||
|
|
||||||
email := utils.NewEmail(
|
|
||||||
eml.GetHeader("Message-Id"),
|
|
||||||
eml.GetHeader("In-Reply-To"),
|
|
||||||
eml.GetHeader("References"),
|
|
||||||
eml.GetHeader("Subject"),
|
|
||||||
s.from,
|
|
||||||
s.to,
|
|
||||||
eml.Text,
|
|
||||||
eml.HTML,
|
|
||||||
files)
|
|
||||||
|
|
||||||
return s.sendmail(email.From, email.To, email.Compose(s.privkey))
|
|
||||||
}
|
}
|
||||||
func (s *outgoingSession) Reset() {}
|
func (s *outgoingSession) Reset() {}
|
||||||
func (s *outgoingSession) Logout() error { return nil }
|
func (s *outgoingSession) Logout() error { return nil }
|
||||||
|
|
||||||
func validateEmail(from, to string, log *logger.Logger, options utils.IncomingFilteringOptions) bool {
|
func validateEmail(from, to string, log *logger.Logger, options email.IncomingFilteringOptions) bool {
|
||||||
enforce := validator.Enforce{
|
enforce := validator.Enforce{
|
||||||
Email: true,
|
Email: true,
|
||||||
MX: options.SpamcheckMX(),
|
MX: options.SpamcheckMX(),
|
||||||
@@ -170,16 +161,3 @@ func validateEmail(from, to string, log *logger.Logger, options utils.IncomingFi
|
|||||||
|
|
||||||
return v.Email(from)
|
return v.Email(from)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAttachments(parts []*enmime.Part, log *logger.Logger) []*utils.File {
|
|
||||||
files := make([]*utils.File, 0, len(parts))
|
|
||||||
for _, attachment := range parts {
|
|
||||||
for _, err := range attachment.Errors {
|
|
||||||
log.Warn("attachment error: %v", err)
|
|
||||||
}
|
|
||||||
file := utils.NewFile(attachment.FileName, attachment.Content)
|
|
||||||
files = append(files, file)
|
|
||||||
}
|
|
||||||
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|||||||
41
utils/mail.go
Normal file
41
utils/mail.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// Mailbox returns mailbox part from email address
|
||||||
|
func Mailbox(email string) string {
|
||||||
|
index := strings.LastIndex(email, "@")
|
||||||
|
if index == -1 {
|
||||||
|
return email
|
||||||
|
}
|
||||||
|
return email[:index]
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmailsList returns human-readable list of mailbox's emails for all available domains
|
||||||
|
func EmailsList(mailbox string, domain string) string {
|
||||||
|
var msg strings.Builder
|
||||||
|
domain = SanitizeDomain(domain)
|
||||||
|
msg.WriteString(mailbox)
|
||||||
|
msg.WriteString("@")
|
||||||
|
msg.WriteString(domain)
|
||||||
|
|
||||||
|
count := len(domains) - 1
|
||||||
|
for i, aliasDomain := range domains {
|
||||||
|
if i < count {
|
||||||
|
msg.WriteString(", ")
|
||||||
|
}
|
||||||
|
if aliasDomain == domain {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
msg.WriteString(mailbox)
|
||||||
|
msg.WriteString("@")
|
||||||
|
msg.WriteString(aliasDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hostname returns hostname part from email address
|
||||||
|
func Hostname(email string) string {
|
||||||
|
return email[strings.LastIndex(email, "@")+1:]
|
||||||
|
}
|
||||||
@@ -22,44 +22,6 @@ func SetDomains(slice []string) {
|
|||||||
domains = slice
|
domains = slice
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mailbox returns mailbox part from email address
|
|
||||||
func Mailbox(email string) string {
|
|
||||||
index := strings.LastIndex(email, "@")
|
|
||||||
if index == -1 {
|
|
||||||
return email
|
|
||||||
}
|
|
||||||
return email[:index]
|
|
||||||
}
|
|
||||||
|
|
||||||
// EmailsList returns human-readable list of mailbox's emails for all available domains
|
|
||||||
func EmailsList(mailbox string, domain string) string {
|
|
||||||
var msg strings.Builder
|
|
||||||
domain = SanitizeDomain(domain)
|
|
||||||
msg.WriteString(mailbox)
|
|
||||||
msg.WriteString("@")
|
|
||||||
msg.WriteString(domain)
|
|
||||||
|
|
||||||
count := len(domains) - 1
|
|
||||||
for i, aliasDomain := range domains {
|
|
||||||
if i < count {
|
|
||||||
msg.WriteString(", ")
|
|
||||||
}
|
|
||||||
if aliasDomain == domain {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
msg.WriteString(mailbox)
|
|
||||||
msg.WriteString("@")
|
|
||||||
msg.WriteString(aliasDomain)
|
|
||||||
}
|
|
||||||
|
|
||||||
return msg.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hostname returns hostname part from email address
|
|
||||||
func Hostname(email string) string {
|
|
||||||
return email[strings.LastIndex(email, "@")+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// SanitizeDomain checks that input domain is available for use
|
// SanitizeDomain checks that input domain is available for use
|
||||||
func SanitizeDomain(domain string) string {
|
func SanitizeDomain(domain string) string {
|
||||||
domain = strings.TrimSpace(domain)
|
domain = strings.TrimSpace(domain)
|
||||||
|
|||||||
Reference in New Issue
Block a user