diff options
Diffstat (limited to 'utils')
| -rw-r--r-- | utils/email/messages.go | 230 | ||||
| -rw-r--r-- | utils/storage/minio.go | 170 |
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 +} |
