summaryrefslogtreecommitdiff
path: root/repository
diff options
context:
space:
mode:
authorBobby <[email protected]>2025-12-24 13:50:07 +0530
committerBobby <[email protected]>2025-12-24 13:50:07 +0530
commitb77d75f05fb2059389c05f6c01484e0cd12e796e (patch)
treee3c5521bf9ed3fcffd59960053d651091496a7ea /repository
parent81ab367f440d6f85297b2013d0c1aa57fda7e9cd (diff)
downloadlain-b77d75f05fb2059389c05f6c01484e0cd12e796e.tar.xz
lain-b77d75f05fb2059389c05f6c01484e0cd12e796e.zip
feat: introduce email folder synchronization and management, refactor email services, and update UI styles
Diffstat (limited to 'repository')
-rw-r--r--repository/email.go284
-rw-r--r--repository/emails.go85
-rw-r--r--repository/folders.go277
3 files changed, 127 insertions, 519 deletions
diff --git a/repository/email.go b/repository/email.go
deleted file mode 100644
index 9203e40..0000000
--- a/repository/email.go
+++ /dev/null
@@ -1,284 +0,0 @@
-package repository
-
-import (
- "fmt"
- "lain/database"
- "lain/models"
- "lain/utils/crypto"
- "lain/utils/email"
- "lain/utils/storage"
- "net/url"
- "strings"
-
- "github.com/gofiber/fiber/v2"
-)
-
-func GetEmails(userEmail, folderPath string, limit int, offset int) ([]fiber.Map, error) {
- // Decode URL-encoded path (e.g., "inbox/lets%20encrypt" -> "inbox/lets encrypt")
- decodedPath, _ := url.QueryUnescape(folderPath)
-
- var folder models.Folder
- err := database.DB.Where("user_email = ? AND LOWER(imap_name) = ?", userEmail, strings.ToLower(decodedPath)).First(&folder).Error
- if err != nil {
- return nil, fmt.Errorf("folder not found: %w", err)
- }
-
- var count int64
- database.DB.Model(&models.Email{}).Where("user_email = ? AND folder_id = ?", userEmail, folder.ID).Count(&count)
-
- // Always sync if no emails exist
- if count == 0 {
- if err := syncEmails(userEmail, folder.ID, folder.IMAPName); err != nil {
- // Log the error but continue to show UI
- fmt.Printf("Failed to sync emails for folder %s: %v\n", folder.IMAPName, err)
- }
- // Recount after sync
- database.DB.Model(&models.Email{}).Where("user_email = ? AND folder_id = ?", userEmail, folder.ID).Count(&count)
- }
-
- var messages []models.Email
- err = database.DB.Where("user_email = ? AND folder_id = ?", userEmail, folder.ID).
- Order("date DESC").
- Limit(limit).
- Offset(offset).
- Find(&messages).Error
-
- if err != nil {
- return nil, fmt.Errorf("failed to fetch emails: %w", err)
- }
-
- var emailMaps []fiber.Map
- for _, message := range messages {
- emailMaps = append(emailMaps, fiber.Map{
- "ID": message.ID,
- "UID": message.UID,
- "From": message.From,
- "FromName": message.FromName,
- "Subject": message.Subject,
- "Date": message.Date,
- "Snippet": message.Snippet,
- "IsRead": message.IsRead,
- "IsFlagged": message.IsFlagged,
- "HasAttachment": message.HasAttachment,
- })
- }
-
- return emailMaps, nil
-}
-
-func GetEmail(userEmail string, emailID uint) (*models.Email, error) {
- var message models.Email
- err := database.DB.Preload("Folder").Where("user_email = ? AND id = ?", userEmail, emailID).First(&message).Error
- if err != nil {
- return nil, fmt.Errorf("email not found: %w", err)
- }
-
- return &message, nil
-}
-
-func GetAttachments(emailID uint) ([]models.Attachment, error) {
- var attachments []models.Attachment
- err := database.DB.Where("email_id = ?", emailID).Find(&attachments).Error
- if err != nil {
- return nil, fmt.Errorf("failed to fetch attachments: %w", err)
- }
-
- return attachments, nil
-}
-
-func MarkEmailAsRead(userEmail string, emailID uint) error {
- var message models.Email
- err := database.DB.Preload("Folder").Where("user_email = ? AND id = ?", userEmail, emailID).First(&message).Error
- if err != nil {
- return fmt.Errorf("email not found: %w", err)
- }
-
- if message.IsRead {
- return nil
- }
-
- var prefs models.Preferences
- if err := database.DB.Where("email = ?", userEmail).First(&prefs).Error; err != nil {
- return err
- }
-
- password, err := crypto.Decrypt(prefs.Authorization)
- if err != nil {
- return err
- }
-
- client, err := email.ConnectIMAP(userEmail, password)
- if err != nil {
- return err
- }
- defer email.DisconnectIMAP(client)
-
- if err := email.MarkAsRead(client, message.Folder.IMAPName, message.UID); err != nil {
- return err
- }
-
- message.IsRead = true
- if err := database.DB.Save(&message).Error; err != nil {
- return fmt.Errorf("failed to update email: %w", err)
- }
-
- return nil
-}
-
-func ToggleEmailFlag(userEmail string, emailID uint) error {
- var message models.Email
- err := database.DB.Preload("Folder").Where("user_email = ? AND id = ?", userEmail, emailID).First(&message).Error
- if err != nil {
- return fmt.Errorf("email not found: %w", err)
- }
-
- var prefs models.Preferences
- if err := database.DB.Where("email = ?", userEmail).First(&prefs).Error; err != nil {
- return err
- }
-
- password, err := crypto.Decrypt(prefs.Authorization)
- if err != nil {
- return err
- }
-
- client, err := email.ConnectIMAP(userEmail, password)
- if err != nil {
- return err
- }
- defer email.DisconnectIMAP(client)
-
- if err := email.ToggleFlag(client, message.Folder.IMAPName, message.UID, message.IsFlagged); err != nil {
- return err
- }
-
- message.IsFlagged = !message.IsFlagged
- if err := database.DB.Save(&message).Error; err != nil {
- return fmt.Errorf("failed to update email: %w", err)
- }
-
- return nil
-}
-
-func syncEmails(userEmail string, folderID uint, folderPath string) error {
- var prefs models.Preferences
- if err := database.DB.Where("email = ?", userEmail).First(&prefs).Error; err != nil {
- return err
- }
-
- password, err := crypto.Decrypt(prefs.Authorization)
- if err != nil {
- return err
- }
-
- client, err := email.ConnectIMAP(userEmail, password)
- if err != nil {
- return err
- }
- defer email.DisconnectIMAP(client)
-
- messages, err := email.FetchMessages(client, folderPath, 50)
- if err != nil {
- return fmt.Errorf("failed to fetch messages: %w", err)
- }
-
- for _, msg := range messages {
- var existingMessage models.Email
- result := database.DB.Where("user_email = ? AND folder_id = ? AND uid = ?", userEmail, folderID, msg.UID).First(&existingMessage)
-
- if result.Error == nil {
- continue
- }
-
- snippet := generateSnippet(msg.BodyText, msg.BodyHTML)
-
- message := models.Email{
- UserEmail: userEmail,
- FolderID: folderID,
- UID: msg.UID,
- MessageID: msg.MessageID,
- From: msg.From,
- FromName: msg.FromName,
- To: strings.Join(msg.To, ", "),
- CC: strings.Join(msg.CC, ", "),
- BCC: strings.Join(msg.BCC, ", "),
- ReplyTo: strings.Join(msg.ReplyTo, ", "),
- Subject: msg.Subject,
- Date: msg.Date,
- BodyText: msg.BodyText,
- BodyHTML: msg.BodyHTML,
- Snippet: snippet,
- Size: int64(msg.Size),
- InReplyTo: msg.InReplyTo,
- IsRead: msg.IsRead,
- IsFlagged: msg.IsFlagged,
- IsAnswered: msg.IsAnswered,
- IsDraft: msg.IsDraft,
- HasAttachment: msg.HasAttachment,
- }
-
- if err := database.DB.Create(&message).Error; err != nil {
- continue
- }
-
- for _, att := range msg.Attachments {
- path, err := storage.UploadAttachment(userEmail, message.ID, att.Filename, att.Data, att.ContentType)
- if err != nil {
- continue
- }
-
- attachment := models.Attachment{
- EmailID: message.ID,
- Filename: att.Filename,
- ContentType: att.ContentType,
- Size: int64(len(att.Data)),
- MinIOPath: path,
- }
-
- database.DB.Create(&attachment)
- }
- }
-
- return nil
-}
-
-func generateSnippet(bodyText, bodyHTML string) string {
- text := bodyText
- if text == "" && bodyHTML != "" {
- text = stripHTML(bodyHTML)
- }
-
- text = strings.TrimSpace(text)
- if len(text) > 150 {
- text = text[:150] + "..."
- }
-
- return text
-}
-
-func stripHTML(html string) string {
- text := html
- text = strings.ReplaceAll(text, "<br>", "\n")
- text = strings.ReplaceAll(text, "<br/>", "\n")
- text = strings.ReplaceAll(text, "<br />", "\n")
- text = strings.ReplaceAll(text, "</p>", "\n\n")
- text = strings.ReplaceAll(text, "</div>", "\n")
-
- inTag := false
- var result strings.Builder
- for _, char := range text {
- if char == '<' {
- inTag = true
- continue
- }
- if char == '>' {
- inTag = false
- continue
- }
- if !inTag {
- result.WriteRune(char)
- }
- }
-
- return strings.TrimSpace(result.String())
-}
diff --git a/repository/emails.go b/repository/emails.go
new file mode 100644
index 0000000..e39684c
--- /dev/null
+++ b/repository/emails.go
@@ -0,0 +1,85 @@
+package repository
+
+import (
+ "fmt"
+ "lain/database"
+ "lain/models"
+)
+
+func GetEmailsByFolder(userEmail string, folderID uint, limit int, offset int) ([]models.Email, error) {
+ var messages []models.Email
+ err := database.DB.Where("user_email = ? AND folder_id = ?", userEmail, folderID).
+ Order("date DESC").
+ Limit(limit).
+ Offset(offset).
+ Find(&messages).Error
+
+ if err != nil {
+ return nil, fmt.Errorf("failed to fetch emails: %w", err)
+ }
+
+ return messages, nil
+}
+
+func GetEmailByID(userEmail string, emailID uint) (*models.Email, error) {
+ var message models.Email
+ err := database.DB.Preload("Folder").Where("user_email = ? AND id = ?", userEmail, emailID).First(&message).Error
+ if err != nil {
+ return nil, fmt.Errorf("email not found: %w", err)
+ }
+
+ return &message, nil
+}
+
+func EmailExists(userEmail string, folderID uint, uid uint32) (bool, error) {
+ var count int64
+ err := database.DB.Model(&models.Email{}).
+ Where("user_email = ? AND folder_id = ? AND uid = ?", userEmail, folderID, uid).
+ Count(&count).Error
+
+ if err != nil {
+ return false, err
+ }
+
+ return count > 0, nil
+}
+
+func CreateEmail(message *models.Email) (*models.Email, error) {
+ if err := database.DB.Create(message).Error; err != nil {
+ return nil, fmt.Errorf("failed to create email: %w", err)
+ }
+ return message, nil
+}
+
+func UpdateEmail(message *models.Email) error {
+ if err := database.DB.Save(message).Error; err != nil {
+ return fmt.Errorf("failed to update email: %w", err)
+ }
+ return nil
+}
+
+func GetAttachmentsByEmailID(emailID uint) ([]models.Attachment, error) {
+ var attachments []models.Attachment
+ err := database.DB.Where("email_id = ?", emailID).Find(&attachments).Error
+ if err != nil {
+ return nil, fmt.Errorf("failed to fetch attachments: %w", err)
+ }
+
+ return attachments, nil
+}
+
+func CreateAttachment(attachment *models.Attachment) error {
+ if err := database.DB.Create(attachment).Error; err != nil {
+ return fmt.Errorf("failed to create attachment: %w", err)
+ }
+ return nil
+}
+
+func CountEmailsInFolder(userEmail string, folderID uint) (int64, error) {
+ var count int64
+ err := database.DB.Model(&models.Email{}).
+ Where("user_email = ? AND folder_id = ?", userEmail, folderID).
+ Count(&count).Error
+
+ return count, err
+}
diff --git a/repository/folders.go b/repository/folders.go
index 20dcaa2..45c20c7 100644
--- a/repository/folders.go
+++ b/repository/folders.go
@@ -1,11 +1,11 @@
package repository
import (
+ "fmt"
"lain/cache"
"lain/database"
"lain/models"
"lain/types"
- "lain/utils/crypto"
"lain/utils/email"
"net/url"
"strings"
@@ -13,7 +13,7 @@ import (
"github.com/gofiber/fiber/v2"
)
-var folderIcons = map[string]types.FolderIconVariant{
+var FolderIcons = map[string]types.FolderIconVariant{
"default": {
Open: "/static/icons/folder_open.png",
Close: "/static/icons/folder.png",
@@ -68,28 +68,54 @@ var folderIcons = map[string]types.FolderIconVariant{
},
}
-func GetFolders(userEmail, activeFolder string) []fiber.Map {
- if cached, ok := cache.GetFolders(userEmail); ok {
- return updateActiveFolder(cached, activeFolder)
+func GetAllFolders(userEmail string) ([]models.Folder, error) {
+ var folders []models.Folder
+ err := database.DB.Where("user_email = ?", userEmail).Find(&folders).Error
+ return folders, err
+}
+
+func GetFolderByIMAPName(userEmail, imapName string) (*models.Folder, error) {
+ var folder models.Folder
+ err := database.DB.Where("user_email = ? AND LOWER(imap_name) = ?", userEmail, strings.ToLower(imapName)).First(&folder).Error
+ if err != nil {
+ return nil, err
}
+ return &folder, nil
+}
- var count int64
- database.DB.Model(&models.Folder{}).Where("user_email = ?", userEmail).Count(&count)
+func CreateFolder(folder *models.Folder) (*models.Folder, error) {
+ if err := database.DB.Create(folder).Error; err != nil {
+ return nil, fmt.Errorf("failed to create folder: %w", err)
+ }
+ return folder, nil
+}
- if count == 0 {
- syncFolders(userEmail)
+func UpdateFolder(folder *models.Folder) error {
+ if err := database.DB.Save(folder).Error; err != nil {
+ return fmt.Errorf("failed to update folder: %w", err)
}
+ return nil
+}
+
+func CountFolders(userEmail string) (int64, error) {
+ var count int64
+ err := database.DB.Model(&models.Folder{}).Where("user_email = ?", userEmail).Count(&count).Error
+ return count, err
+}
- var allFolders []models.Folder
- database.DB.Where("user_email = ?", userEmail).Find(&allFolders)
+func BuildFolderTree(userEmail, activeFolder string) []fiber.Map {
+ if cached, ok := cache.GetFolders(userEmail); ok {
+ return email.UpdateActiveFolder(cached, activeFolder)
+ }
- sortFolders(allFolders)
+ allFolders, _ := GetAllFolders(userEmail)
+ email.SortFolders(allFolders)
folderMap := make(map[uint]*fiber.Map)
var rootFolders []fiber.Map
for _, folder := range allFolders {
- displayName := getDisplayName(folder.IMAPName)
+ displayName := email.GetDisplayName(folder.IMAPName)
folderData := fiber.Map{
"ID": folder.ID,
@@ -119,25 +145,13 @@ func GetFolders(userEmail, activeFolder string) []fiber.Map {
}
cache.SetFolders(userEmail, rootFolders)
-
- return updateActiveFolder(rootFolders, activeFolder)
-}
-
-func RefreshFolders(userEmail string) error {
- cache.InvalidateFolders(userEmail)
- err := syncFolders(userEmail)
- if err != nil {
- return err
- }
- return nil
+ return email.UpdateActiveFolder(rootFolders, activeFolder)
}
func GetFolderDisplayName(userEmail, folderPath string) string {
decodedPath, _ := url.QueryUnescape(folderPath)
- var folder models.Folder
- err := database.DB.Where("user_email = ? AND LOWER(imap_name) = ?", userEmail, strings.ToLower(decodedPath)).First(&folder).Error
-
+ folder, err := GetFolderByIMAPName(userEmail, decodedPath)
if err != nil {
if strings.ToLower(decodedPath) == "inbox" {
return "Inbox"
@@ -145,212 +159,5 @@ func GetFolderDisplayName(userEmail, folderPath string) string {
return decodedPath
}
- return getDisplayName(folder.IMAPName)
-}
-
-func getDisplayName(imapName string) string {
- if strings.Contains(imapName, "/") {
- parts := strings.Split(imapName, "/")
- lastPart := parts[len(parts)-1]
- if strings.ToLower(lastPart) == "inbox" {
- return "Inbox"
- }
- return lastPart
- }
-
- if strings.ToLower(imapName) == "inbox" {
- return "Inbox"
- }
-
- return imapName
-}
-
-func updateActiveFolder(folders []fiber.Map, activeFolder string) []fiber.Map {
- decodedActive, _ := url.QueryUnescape(activeFolder)
- activeLower := strings.ToLower(decodedActive)
-
- foldersCopy := make([]fiber.Map, len(folders))
- for i, folder := range folders {
- foldersCopy[i] = copyFolderMap(folder)
- }
-
- var updateActive func([]fiber.Map)
- updateActive = func(folderList []fiber.Map) {
- for i := range folderList {
- imapNameLower := strings.ToLower(folderList[i]["IMAPName"].(string))
- folderList[i]["Active"] = imapNameLower == activeLower
- if subfolders, ok := folderList[i]["Subfolders"].([]fiber.Map); ok && len(subfolders) > 0 {
- updateActive(subfolders)
- }
- }
- }
-
- updateActive(foldersCopy)
- return foldersCopy
-}
-
-func copyFolderMap(folder fiber.Map) fiber.Map {
- copy := fiber.Map{}
- for k, v := range folder {
- if k == "Subfolders" {
- if subfolders, ok := v.([]fiber.Map); ok {
- subfoldersCopy := make([]fiber.Map, len(subfolders))
- for i, sf := range subfolders {
- subfoldersCopy[i] = copyFolderMap(sf)
- }
- copy[k] = subfoldersCopy
- }
- } else {
- copy[k] = v
- }
- }
- return copy
-}
-
-func sortFolders(folders []models.Folder) {
- for i := 0; i < len(folders)-1; i++ {
- for j := 0; j < len(folders)-i-1; j++ {
- if folders[j].SortOrder > folders[j+1].SortOrder {
- folders[j], folders[j+1] = folders[j+1], folders[j]
- } else if folders[j].SortOrder == folders[j+1].SortOrder {
- if strings.ToLower(folders[j].IMAPName) > strings.ToLower(folders[j+1].IMAPName) {
- folders[j], folders[j+1] = folders[j+1], folders[j]
- }
- }
- }
- }
-}
-
-func getFolderType(folderName string) string {
- nameLower := strings.ToLower(folderName)
-
- if strings.Contains(folderName, "/") {
- parts := strings.Split(folderName, "/")
- nameLower = strings.ToLower(parts[len(parts)-1])
- }
-
- for iconType := range folderIcons {
- if iconType == "default" {
- continue
- }
- if strings.Contains(nameLower, iconType) {
- return iconType
- }
- }
-
- return "default"
-}
-
-func syncFolders(userEmail string) error {
- var prefs models.Preferences
- if err := database.DB.Where("email = ?", userEmail).First(&prefs).Error; err != nil {
- return err
- }
-
- password, err := crypto.Decrypt(prefs.Authorization)
- if err != nil {
- return err
- }
-
- client, err := email.ConnectIMAP(userEmail, password)
- if err != nil {
- return err
- }
- defer email.DisconnectIMAP(client)
-
- imapFolders, err := email.FetchFolders(client)
- if err != nil {
- return err
- }
-
- foldersByName := make(map[string]uint)
-
- for i, imapFolder := range imapFolders {
- if strings.HasPrefix(imapFolder.Name, "Virtual") || strings.Contains(imapFolder.Name, "/Virtual") {
- continue
- }
-
- var folder models.Folder
- imapNameLower := strings.ToLower(imapFolder.Name)
- result := database.DB.Where("user_email = ? AND LOWER(imap_name) = ?", userEmail, imapNameLower).First(&folder)
-
- sortOrder := getSortOrder(imapFolder.Name, i)
- folderType := getFolderType(imapFolder.Name)
- iconVariant := folderIcons[folderType]
-
- if result.Error != nil {
- folder = models.Folder{
- UserEmail: userEmail,
- Name: imapFolder.Name,
- IMAPName: imapFolder.Name,
- IconOpen: iconVariant.Open,
- IconClose: iconVariant.Close,
- SortOrder: sortOrder,
- }
- database.DB.Create(&folder)
- foldersByName[imapNameLower] = folder.ID
- } else {
- folder.Name = imapFolder.Name
- folder.SortOrder = sortOrder
- folder.IconOpen = iconVariant.Open
- folder.IconClose = iconVariant.Close
- database.DB.Save(&folder)
- foldersByName[imapNameLower] = folder.ID
- }
- }
-
- for _, imapFolder := range imapFolders {
- if strings.HasPrefix(imapFolder.Name, "Virtual") || strings.Contains(imapFolder.Name, "/Virtual") {
- continue
- }
-
- if strings.Contains(imapFolder.Name, "/") {
- parts := strings.Split(imapFolder.Name, "/")
- if len(parts) > 1 {
- parentName := strings.Join(parts[:len(parts)-1], "/")
- parentNameLower := strings.ToLower(parentName)
- if parentID, ok := foldersByName[parentNameLower]; ok {
- var folder models.Folder
- imapNameLower := strings.ToLower(imapFolder.Name)
- if err := database.DB.Where("user_email = ? AND LOWER(imap_name) = ?", userEmail, imapNameLower).First(&folder).Error; err == nil {
- folder.ParentID = &parentID
- database.DB.Save(&folder)
- }
- }
- }
- }
- }
-
- return nil
-}
-
-func getSortOrder(folderName string, index int) int {
- nameLower := strings.ToLower(folderName)
-
- if nameLower == "inbox" {
- return 0
- }
- if strings.Contains(nameLower, "draft") {
- return 1
- }
- if strings.Contains(nameLower, "sent") {
- return 2
- }
- if strings.Contains(nameLower, "archive") {
- return 3
- }
- if strings.Contains(nameLower, "trash") || strings.Contains(nameLower, "deleted") {
- return 4
- }
- if strings.Contains(nameLower, "spam") || strings.Contains(nameLower, "junk") {
- return 5
- }
-
- if strings.Contains(folderName, "/") {
- parts := strings.Split(folderName, "/")
- baseOrder := getSortOrder(parts[0], index)
- return baseOrder + 1000 + (index * 10)
- }
-
- return 100 + index
+ return email.GetDisplayName(folder.IMAPName)
}