Files
plane/email/pkg/services/session.go
Nikhil f42ae0eaf9 [WEB-3768] feat: add support for attachment for intake issues created from email (#2903)
* chore: intake email attachment

* chore: add rate limit for intake form issues

* chore: add attachments for intake issues

* chore: add logging in email service

* chore: remove patch endpoint url

* chore: remove private key

* chore: update formatting

* chore: move intake anchor function to separate utility file

* chore: added anchor based rate limiting

* chore: update rate limit
2025-04-09 15:12:04 +05:30

151 lines
3.5 KiB
Go

package services
import (
"bytes"
"errors"
"io"
"plane/email/pkg/logger"
"plane/email/pkg/utils/parser"
"strings"
"github.com/emersion/go-message/mail"
"github.com/emersion/go-smtp"
)
type Session struct {
From string
To []string
}
func (s *Session) AuthPlain(username, password string) error {
// log.Println("Authenticating user", username)
if AuthenticateUser(username, password) {
return nil
}
return errors.New("invalid username or password")
}
func (s *Session) Mail(from string, opts *smtp.MailOptions) error {
s.From = from
// log.Printf("Mail from: %s", from)
return nil
}
func (s *Session) Rcpt(to string, opts *smtp.RcptOptions) error {
// log.Printf("Rcpt to: %s", to)
s.To = append(s.To, to)
return nil
}
func (s *Session) Data(r io.Reader) error {
// log.Println("Receiving message")
if s.From == "" || len(s.To) == 0 {
logger.Log.Errorf("missing from or to address")
return errors.New("missing from or to address")
}
// Read email data
var buf bytes.Buffer
if _, err := io.Copy(&buf, r); err != nil {
logger.Log.Errorf("failed to read email data: %v", err)
return err
}
data := buf.Bytes()
// Basic spam check
if parser.IsBacklistedDomain(string(s.From)) {
logger.Log.Errorf("Detected spam domain from %s", s.From)
return errors.New("message rejected")
}
if parser.IsSpam(string(data)) {
logger.Log.Errorf("Detected spam from %s", s.From)
return errors.New("message rejected")
}
// Parse the email
mr, err := mail.CreateReader(bytes.NewReader(data))
if err != nil {
logger.Log.Errorf("failed to parse email: %v", err)
return err
}
header := mr.Header
subject, _ := header.Subject()
var attachments []Attachment // Custom struct to hold attachment metadata
var plainBody, htmlBody string
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
logger.Log.Errorf("failed to read email part: %v", err)
return err
}
switch h := p.Header.(type) {
case *mail.InlineHeader:
// Check the content type of this part
contentType, _, _ := h.ContentType()
b, _ := io.ReadAll(p.Body)
if contentType == "text/plain" {
plainBody = string(b)
} else if contentType == "text/html" {
htmlBody = string(b)
}
case *mail.AttachmentHeader:
filename, _ := h.Filename()
contentType, _, _ := h.ContentType()
content, err := io.ReadAll(p.Body)
if err != nil {
logger.Log.Errorf("failed to read attachment %s: %v", filename, err)
continue
}
logger.Log.Infof("Received attachment: %s (%s)", filename, contentType)
// Save or process the attachment
attachments = append(attachments, Attachment{
Name: filename,
Type: contentType,
Size: len(content),
Content: content,
})
}
}
// get attachment urls
attachmentIDs, err := UploadAttachment(attachments, strings.Join(s.To, ", "), s.From)
// update attachment IDs
if err != nil {
logger.Log.Errorf("failed to upload attachments: %v", err)
return err
}
// Prefer HTML if available, otherwise use plain text
body := plainBody
if htmlBody != "" {
body = htmlBody // Or consider storing both separately
}
// Store the email
if err := SaveEmail(s.From, strings.Join(s.To, ", "), subject, body, attachmentIDs); err != nil {
logger.Log.Errorf("failed to save email: %v", err)
return err
}
logger.Log.Infof("Email processed successfully: %s", s.From)
return nil
}
func (s *Session) Reset() {
s.From = ""
s.To = nil
}
func (s *Session) Logout() error {
return nil
}