summaryrefslogtreecommitdiff
path: root/utils
diff options
context:
space:
mode:
Diffstat (limited to 'utils')
-rw-r--r--utils/email/messages.go230
-rw-r--r--utils/storage/minio.go170
2 files changed, 400 insertions, 0 deletions
diff --git a/utils/email/messages.go b/utils/email/messages.go
new file mode 100644
index 0000000..cedbec5
--- /dev/null
+++ b/utils/email/messages.go
@@ -0,0 +1,230 @@
+package email
+
+import (
+ "fmt"
+ "io"
+ "lain/types"
+
+ "github.com/emersion/go-imap"
+ "github.com/emersion/go-message/mail"
+)
+
+func SelectFolder(client *types.EmailClient, folderName string) (*imap.MailboxStatus, error) {
+ mbox, err := client.Select(folderName, false)
+ if err != nil {
+ return nil, fmt.Errorf("failed to select folder: %w", err)
+ }
+ return mbox, nil
+}
+
+func FetchMessages(client *types.EmailClient, folderName string, limit uint32) ([]*types.EmailMessage, error) {
+ mbox, err := SelectFolder(client, folderName)
+ if err != nil {
+ return nil, err
+ }
+
+ if mbox.Messages == 0 {
+ return []*types.EmailMessage{}, nil
+ }
+
+ from := uint32(1)
+ to := mbox.Messages
+ if mbox.Messages > limit {
+ from = mbox.Messages - limit + 1
+ }
+
+ seqset := new(imap.SeqSet)
+ seqset.AddRange(from, to)
+
+ messages := make(chan *imap.Message, 10)
+ done := make(chan error, 1)
+
+ section := &imap.BodySectionName{}
+ items := []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchUid, imap.FetchRFC822Size, section.FetchItem()}
+
+ go func() {
+ done <- client.Fetch(seqset, items, messages)
+ }()
+
+ var result []*types.EmailMessage
+ for msg := range messages {
+ if msg == nil {
+ continue
+ }
+
+ emailMsg, err := parseMessage(msg)
+ if err != nil {
+ continue
+ }
+
+ result = append(result, emailMsg)
+ }
+
+ if err := <-done; err != nil {
+ return nil, fmt.Errorf("failed to fetch messages: %w", err)
+ }
+
+ return result, nil
+}
+
+func parseMessage(msg *imap.Message) (*types.EmailMessage, error) {
+ if msg.Envelope == nil {
+ return nil, fmt.Errorf("message envelope is nil")
+ }
+
+ section := &imap.BodySectionName{}
+ bodyReader := msg.GetBody(section)
+ if bodyReader == nil {
+ return nil, fmt.Errorf("message body is nil")
+ }
+
+ mr, err := mail.CreateReader(bodyReader)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create mail reader: %w", err)
+ }
+
+ var bodyText, bodyHTML string
+ var attachments []types.EmailAttachment
+
+ for {
+ part, err := mr.NextPart()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ continue
+ }
+
+ switch h := part.Header.(type) {
+ case *mail.InlineHeader:
+ contentType, _, _ := h.ContentType()
+ body, _ := io.ReadAll(part.Body)
+
+ if contentType == "text/plain" {
+ bodyText = string(body)
+ } else if contentType == "text/html" {
+ bodyHTML = string(body)
+ }
+
+ case *mail.AttachmentHeader:
+ filename, _ := h.Filename()
+ contentType, _, _ := h.ContentType()
+ data, _ := io.ReadAll(part.Body)
+
+ attachments = append(attachments, types.EmailAttachment{
+ Filename: filename,
+ ContentType: contentType,
+ Data: data,
+ })
+ }
+ }
+
+ var fromAddr, fromName string
+ if len(msg.Envelope.From) > 0 {
+ fromAddr = msg.Envelope.From[0].MailboxName + "@" + msg.Envelope.From[0].HostName
+ fromName = msg.Envelope.From[0].PersonalName
+ }
+
+ var toList []string
+ for _, addr := range msg.Envelope.To {
+ toList = append(toList, addr.MailboxName+"@"+addr.HostName)
+ }
+
+ var ccList []string
+ for _, addr := range msg.Envelope.Cc {
+ ccList = append(ccList, addr.MailboxName+"@"+addr.HostName)
+ }
+
+ var bccList []string
+ for _, addr := range msg.Envelope.Bcc {
+ bccList = append(bccList, addr.MailboxName+"@"+addr.HostName)
+ }
+
+ var replyToList []string
+ for _, addr := range msg.Envelope.ReplyTo {
+ replyToList = append(replyToList, addr.MailboxName+"@"+addr.HostName)
+ }
+
+ isRead := false
+ isFlagged := false
+ isAnswered := false
+ isDraft := false
+
+ for _, flag := range msg.Flags {
+ switch flag {
+ case imap.SeenFlag:
+ isRead = true
+ case imap.FlaggedFlag:
+ isFlagged = true
+ case imap.AnsweredFlag:
+ isAnswered = true
+ case imap.DraftFlag:
+ isDraft = true
+ }
+ }
+
+ return &types.EmailMessage{
+ UID: msg.Uid,
+ MessageID: msg.Envelope.MessageId,
+ From: fromAddr,
+ FromName: fromName,
+ To: toList,
+ CC: ccList,
+ BCC: bccList,
+ ReplyTo: replyToList,
+ Subject: msg.Envelope.Subject,
+ Date: msg.Envelope.Date,
+ BodyText: bodyText,
+ BodyHTML: bodyHTML,
+ Size: msg.Size,
+ InReplyTo: msg.Envelope.InReplyTo,
+ IsRead: isRead,
+ IsFlagged: isFlagged,
+ IsAnswered: isAnswered,
+ IsDraft: isDraft,
+ HasAttachment: len(attachments) > 0,
+ Attachments: attachments,
+ }, nil
+}
+
+func MarkAsRead(client *types.EmailClient, folderName string, uid uint32) error {
+ if _, err := SelectFolder(client, folderName); err != nil {
+ return err
+ }
+
+ seqSet := new(imap.SeqSet)
+ seqSet.AddNum(uid)
+
+ item := imap.FormatFlagsOp(imap.AddFlags, true)
+ flags := []interface{}{imap.SeenFlag}
+
+ if err := client.UidStore(seqSet, item, flags, nil); err != nil {
+ return fmt.Errorf("failed to mark as read: %w", err)
+ }
+
+ return nil
+}
+
+func ToggleFlag(client *types.EmailClient, folderName string, uid uint32, isFlagged bool) error {
+ if _, err := SelectFolder(client, folderName); err != nil {
+ return err
+ }
+
+ seqSet := new(imap.SeqSet)
+ seqSet.AddNum(uid)
+
+ var item imap.StoreItem
+ if isFlagged {
+ item = imap.FormatFlagsOp(imap.RemoveFlags, true)
+ } else {
+ item = imap.FormatFlagsOp(imap.AddFlags, true)
+ }
+
+ flags := []interface{}{imap.FlaggedFlag}
+
+ if err := client.UidStore(seqSet, item, flags, nil); err != nil {
+ return fmt.Errorf("failed to toggle flag: %w", err)
+ }
+
+ return nil
+}
diff --git a/utils/storage/minio.go b/utils/storage/minio.go
new file mode 100644
index 0000000..0d6e364
--- /dev/null
+++ b/utils/storage/minio.go
@@ -0,0 +1,170 @@
+package storage
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "lain/config"
+ "path/filepath"
+ "time"
+
+ "github.com/minio/minio-go/v7"
+ "github.com/minio/minio-go/v7/pkg/credentials"
+)
+
+var minioClient *minio.Client
+
+func InitMinIO() error {
+ client, err := minio.New(config.MinIO.Endpoint, &minio.Options{
+ Creds: credentials.NewStaticV4(config.MinIO.AccessKey, config.MinIO.SecretKey, ""),
+ Secure: config.MinIO.UseSSL,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to create minio client: %w", err)
+ }
+
+ ctx := context.Background()
+ exists, err := client.BucketExists(ctx, config.MinIO.BucketName)
+ if err != nil {
+ return fmt.Errorf("failed to check bucket existence: %w", err)
+ }
+
+ if !exists {
+ err = client.MakeBucket(ctx, config.MinIO.BucketName, minio.MakeBucketOptions{})
+ if err != nil {
+ return fmt.Errorf("failed to create bucket: %w", err)
+ }
+ }
+
+ minioClient = client
+ return nil
+}
+
+func UploadAttachment(userEmail string, emailID uint, filename string, data []byte, contentType string) (string, error) {
+ if minioClient == nil {
+ return "", fmt.Errorf("minio client not initialized")
+ }
+
+ path := fmt.Sprintf("attachments/%s/%d/%s", userEmail, emailID, filename)
+
+ ctx := context.Background()
+
+ _, err := minioClient.PutObject(
+ ctx,
+ config.MinIO.BucketName,
+ path,
+ bytes.NewReader(data),
+ int64(len(data)),
+ minio.PutObjectOptions{
+ ContentType: contentType,
+ },
+ )
+
+ if err != nil {
+ return "", fmt.Errorf("failed to upload attachment: %w", err)
+ }
+
+ return path, nil
+}
+
+func DownloadAttachment(path string) ([]byte, error) {
+ if minioClient == nil {
+ return nil, fmt.Errorf("minio client not initialized")
+ }
+
+ ctx := context.Background()
+
+ object, err := minioClient.GetObject(ctx, config.MinIO.BucketName, path, minio.GetObjectOptions{})
+ if err != nil {
+ return nil, fmt.Errorf("failed to get attachment object: %w", err)
+ }
+ defer object.Close()
+
+ data, err := io.ReadAll(object)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read attachment data: %w", err)
+ }
+
+ return data, nil
+}
+
+func DeleteAttachment(path string) error {
+ if minioClient == nil {
+ return fmt.Errorf("minio client not initialized")
+ }
+
+ ctx := context.Background()
+
+ err := minioClient.RemoveObject(ctx, config.MinIO.BucketName, path, minio.RemoveObjectOptions{})
+ if err != nil {
+ return fmt.Errorf("failed to delete attachment: %w", err)
+ }
+
+ return nil
+}
+
+func DeleteAttachmentsByEmail(userEmail string, emailID uint) error {
+ if minioClient == nil {
+ return fmt.Errorf("minio client not initialized")
+ }
+
+ ctx := context.Background()
+ prefix := fmt.Sprintf("attachments/%s/%d/", userEmail, emailID)
+
+ objectCh := minioClient.ListObjects(ctx, config.MinIO.BucketName, minio.ListObjectsOptions{
+ Prefix: prefix,
+ Recursive: true,
+ })
+
+ for object := range objectCh {
+ if object.Err != nil {
+ return fmt.Errorf("failed to list attachments: %w", object.Err)
+ }
+
+ err := minioClient.RemoveObject(ctx, config.MinIO.BucketName, object.Key, minio.RemoveObjectOptions{})
+ if err != nil {
+ return fmt.Errorf("failed to delete attachment %s: %w", object.Key, err)
+ }
+ }
+
+ return nil
+}
+
+func GetAttachmentURL(path string, expiryDuration time.Duration) (string, error) {
+ if minioClient == nil {
+ return "", fmt.Errorf("minio client not initialized")
+ }
+
+ ctx := context.Background()
+
+ url, err := minioClient.PresignedGetObject(ctx, config.MinIO.BucketName, path, expiryDuration, nil)
+ if err != nil {
+ return "", fmt.Errorf("failed to generate presigned url: %w", err)
+ }
+
+ return url.String(), nil
+}
+
+func GetAttachmentFilename(path string) string {
+ return filepath.Base(path)
+}
+
+func AttachmentExists(path string) (bool, error) {
+ if minioClient == nil {
+ return false, fmt.Errorf("minio client not initialized")
+ }
+
+ ctx := context.Background()
+
+ _, err := minioClient.StatObject(ctx, config.MinIO.BucketName, path, minio.StatObjectOptions{})
+ if err != nil {
+ errResponse := minio.ToErrorResponse(err)
+ if errResponse.Code == "NoSuchKey" {
+ return false, nil
+ }
+ return false, fmt.Errorf("failed to check attachment existence: %w", err)
+ }
+
+ return true, nil
+}