From 6d027c92ab27f952e2b0200e6bd380d55f0038d2 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Wed, 26 Jun 2024 01:43:17 +0530 Subject: [PATCH] refactor email inbox. --- cmd/init.go | 63 +-- cmd/main.go | 61 +-- frontend/src/assets/styles/main.scss | 49 +-- frontend/src/components/NavBar.vue | 4 +- .../conversation/ConversationSideBar.vue | 16 +- .../src/components/conversation/ReplyBox.vue | 4 +- .../conversationlist/ConversationList.vue | 7 +- .../conversationlist/ConversationListItem.vue | 8 +- .../components/dashboard/agent/CountCards.vue | 34 ++ .../components/message/AgentMessageBubble.vue | 7 +- frontend/src/views/DashboardView.vue | 68 +-- i18n/en.json | 6 +- internal/automation/automation.go | 13 +- internal/conversation/conversations.go | 6 +- internal/conversation/models/models.go | 1 + internal/conversation/queries.sql | 5 +- internal/inbox/channel/email/email.go | 74 ++-- internal/inbox/channel/email/imap.go | 411 +++++++++--------- internal/inbox/channel/email/smtp.go | 2 +- internal/inbox/inbox.go | 42 +- internal/initz/initz.go | 4 +- internal/message/messages.go | 43 +- 22 files changed, 493 insertions(+), 435 deletions(-) create mode 100644 frontend/src/components/dashboard/agent/CountCards.vue diff --git a/cmd/init.go b/cmd/init.go index 9a04dc2b..61135396 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -16,8 +16,8 @@ import ( convtag "github.com/abhinavxd/artemis/internal/conversation/tag" "github.com/abhinavxd/artemis/internal/inbox" "github.com/abhinavxd/artemis/internal/inbox/channel/email" + "github.com/abhinavxd/artemis/internal/initz" "github.com/abhinavxd/artemis/internal/message" - mmodels "github.com/abhinavxd/artemis/internal/message/models" "github.com/abhinavxd/artemis/internal/rbac" "github.com/abhinavxd/artemis/internal/tag" "github.com/abhinavxd/artemis/internal/team" @@ -170,7 +170,6 @@ func initContactManager(db *sqlx.DB, lo *logf.Logger) *contact.Manager { func initMessages(db *sqlx.DB, lo *logf.Logger, - incomingMsgQ chan mmodels.IncomingMessage, wsHub *ws.Hub, userMgr *user.Manager, teaMgr *team.Manager, @@ -179,7 +178,7 @@ func initMessages(db *sqlx.DB, conversationMgr *conversation.Manager, inboxMgr *inbox.Manager, automationEngine *automation.Engine) *message.Manager { - mgr, err := message.New(incomingMsgQ, + mgr, err := message.New( wsHub, userMgr, teaMgr, @@ -189,8 +188,10 @@ func initMessages(db *sqlx.DB, conversationMgr, automationEngine, message.Opts{ - DB: db, - Lo: lo, + DB: db, + Lo: lo, + OutgoingMsgQueueSize: ko.MustInt("message.outgoing_queue_size"), + IncomingMsgQueueSize: ko.MustInt("message.incoming_queue_size"), }) if err != nil { log.Fatalf("error initializing message manager: %v", err) @@ -247,31 +248,12 @@ func initAttachmentsManager(db *sqlx.DB, lo *logf.Logger) *attachment.Manager { return mgr } -// initInboxManager initializes the inbox manager and the `active` inboxes. -func initInboxManager(db *sqlx.DB, lo *logf.Logger, incomingMsgQ chan mmodels.IncomingMessage) *inbox.Manager { - mgr, err := inbox.New(lo, db, incomingMsgQ) +// initInboxManager initializes the inbox manager without registering inboxes. +func initInboxManager(db *sqlx.DB, lo *logf.Logger) *inbox.Manager { + mgr, err := inbox.New(lo, db) if err != nil { log.Fatalf("error initializing inbox manager: %v", err) } - - inboxRecords, err := mgr.GetActiveInboxes() - if err != nil { - log.Fatalf("error fetching active inboxes %v", err) - } - - for _, inboxR := range inboxRecords { - switch inboxR.Channel { - case "email": - log.Printf("initializing `Email` inbox: %s", inboxR.Name) - inbox, err := initEmailInbox(inboxR) - if err != nil { - log.Fatalf("error initializing email inbox %v", err) - } - mgr.Register(inbox) - default: - log.Printf("WARNING: Unknown inbox channel: %s", inboxR.Name) - } - } return mgr } @@ -311,7 +293,7 @@ func initUserFilterMgr(db *sqlx.DB) *filterstore.Manager { } // initEmailInbox initializes the email inbox. -func initEmailInbox(inboxRecord inbox.InboxRecord) (inbox.Inbox, error) { +func initEmailInbox(inboxRecord inbox.InboxRecord, store inbox.MessageStore) (inbox.Inbox, error) { var config email.Config // Load JSON data into Koanf. @@ -334,9 +316,10 @@ func initEmailInbox(inboxRecord inbox.InboxRecord) (inbox.Inbox, error) { // Set from addr. config.From = inboxRecord.From - inbox, err := email.New(email.Opts{ + inbox, err := email.New(store, email.Opts{ ID: inboxRecord.ID, Config: config, + Lo: initz.Logger(ko.MustString("app.log_level"), ko.MustString("app.env"), "email_inbox"), }) if err != nil { @@ -348,3 +331,25 @@ func initEmailInbox(inboxRecord inbox.InboxRecord) (inbox.Inbox, error) { return inbox, nil } + +// registerInboxes registers the active inboxes with the inbox manager. +func registerInboxes(mgr *inbox.Manager, store inbox.MessageStore) { + inboxRecords, err := mgr.GetActiveInboxes() + if err != nil { + log.Fatalf("error fetching active inboxes %v", err) + } + + for _, inboxR := range inboxRecords { + switch inboxR.Channel { + case "email": + log.Printf("initializing `Email` inbox: %s", inboxR.Name) + inbox, err := initEmailInbox(inboxR, store) + if err != nil { + log.Fatalf("error initializing email inbox %v", err) + } + mgr.Register(inbox) + default: + log.Printf("WARNING: Unknown inbox channel: %s", inboxR.Name) + } + } +} diff --git a/cmd/main.go b/cmd/main.go index 8cfe9bd2..6e797ba3 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,6 +6,7 @@ import ( "os" "os/signal" "syscall" + "time" "github.com/abhinavxd/artemis/internal/attachment" "github.com/abhinavxd/artemis/internal/cannedresp" @@ -15,7 +16,6 @@ import ( "github.com/abhinavxd/artemis/internal/inbox" "github.com/abhinavxd/artemis/internal/initz" "github.com/abhinavxd/artemis/internal/message" - "github.com/abhinavxd/artemis/internal/message/models" "github.com/abhinavxd/artemis/internal/rbac" "github.com/abhinavxd/artemis/internal/tag" "github.com/abhinavxd/artemis/internal/team" @@ -60,29 +60,25 @@ func main() { var ( shutdownCh = make(chan struct{}) ctx, stop = signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - - // Incoming messages from all inboxes are pushed to this queue. - incomingMsgQ = make(chan models.IncomingMessage, ko.MustInt("message.incoming_queue_size")) - - lo = initz.Logger(ko.MustString("app.log_level"), ko.MustString("app.env"), "artemis") - rd = initz.Redis(ko) - db = initz.DB(ko) + lo = initz.Logger(ko.MustString("app.log_level"), ko.MustString("app.env"), "artemis") + rd = initz.Redis(ko) + db = initz.DB(ko) wsHub = ws.NewHub() - attachmentMgr = initAttachmentsManager(db, &lo) - cntctMgr = initContactManager(db, &lo) - inboxMgr = initInboxManager(db, &lo, incomingMsgQ) - teamMgr = initTeamMgr(db, &lo) - userMgr = initUserDB(db, &lo) - conversationMgr = initConversations(db, &lo) - automationEngine = initAutomationEngine(db, &lo) - msgMgr = initMessages(db, &lo, incomingMsgQ, wsHub, userMgr, teamMgr, cntctMgr, attachmentMgr, conversationMgr, inboxMgr, automationEngine) - autoAssignerEngine = initAutoAssignmentEngine(teamMgr, conversationMgr, msgMgr, &lo) + attachmentMgr = initAttachmentsManager(db, lo) + cntctMgr = initContactManager(db, lo) + inboxMgr = initInboxManager(db, lo) + teamMgr = initTeamMgr(db, lo) + userMgr = initUserDB(db, lo) + conversationMgr = initConversations(db, lo) + automationEngine = initAutomationEngine(db, lo) + msgMgr = initMessages(db, lo, wsHub, userMgr, teamMgr, cntctMgr, attachmentMgr, conversationMgr, inboxMgr, automationEngine) + autoAssignerEngine = initAutoAssignmentEngine(teamMgr, conversationMgr, msgMgr, lo) ) // Init the app var app = &App{ - lo: &lo, + lo: lo, cntctMgr: cntctMgr, inboxMgr: inboxMgr, userMgr: userMgr, @@ -92,25 +88,28 @@ func main() { msgMgr: msgMgr, constants: initConstants(), rbac: initRBACEngine(db), - tagMgr: initTags(db, &lo), + tagMgr: initTags(db, lo), userFilterMgr: initUserFilterMgr(db), sessMgr: initSessionManager(rd), - cannedRespMgr: initCannedResponse(db, &lo), - conversationTagsMgr: initConversationTags(db, &lo), + cannedRespMgr: initCannedResponse(db, lo), + conversationTagsMgr: initConversationTags(db, lo), } - automationEngine.SetMsgRecorder(app.msgMgr) + // Register all inboxes with the inbox manager. + registerInboxes(inboxMgr, msgMgr) + + automationEngine.SetMsgRecorder(msgMgr) automationEngine.SetConvUpdater(conversationMgr) // Start receivers for all active inboxes. - inboxMgr.Receive() + inboxMgr.Receive(ctx) // Start inserting incoming msgs and dispatch pending outgoing messages. go app.msgMgr.StartDBInserts(ctx, ko.MustInt("message.reader_concurrency")) go app.msgMgr.StartDispatcher(ctx, ko.MustInt("message.dispatch_concurrency"), ko.MustDuration("message.dispatch_read_interval")) // Start automation rule engine. - go automationEngine.Serve() + go automationEngine.Serve(ctx) // Start conversation auto assigner engine. go autoAssignerEngine.Serve(ctx, ko.MustDuration("autoassigner.assign_interval")) @@ -133,17 +132,25 @@ func main() { ReadBufferSize: ko.MustInt("app.server.max_body_size"), } - // Goroutine for handling interrupt signals & gracefully shutting down the server. + // Handling graceful shutdown with a delay go func() { + // Wait for the interruption signal <-ctx.Done() + + log.Printf("\x1b[%dm%s\x1b[0m", 31, "Shutting down the server please wait...") + + // Additional grace period before triggering shutdown + time.Sleep(7 * time.Second) + + // Signal to shutdown the server shutdownCh <- struct{}{} stop() }() - // Start the HTTP server. + // Starting the server and waiting for the shutdown signal log.Printf("🚀 server listening on %s %s", ko.String("app.server.address"), ko.String("app.server.socket")) if err := g.ListenServeAndWaitGracefully(ko.String("app.server.address"), ko.String("server.socket"), s, shutdownCh); err != nil { log.Fatalf("error starting frontend server: %v", err) } - log.Println("bye") + log.Println("Server shutdown completed") } diff --git a/frontend/src/assets/styles/main.scss b/frontend/src/assets/styles/main.scss index d6f20537..f03f712c 100644 --- a/frontend/src/assets/styles/main.scss +++ b/frontend/src/assets/styles/main.scss @@ -2,8 +2,6 @@ @tailwind components; @tailwind utilities; - - // App default font-size. // Default: 16px, 15px looked very wide. :root { @@ -24,7 +22,6 @@ $editorContainerId: 'editor-container'; -// --primary: 217 88.1% 60.4%; // Theme. @@ -39,7 +36,7 @@ $editorContainerId: 'editor-container'; --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; - --primary: 221.2 83.2% 53.3%; + --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96.1%; @@ -56,8 +53,8 @@ $editorContainerId: 'editor-container'; --border:214.3 31.8% 91.4%; --input:214.3 31.8% 91.4%; - --ring:221.2 83.2% 53.3%; - --radius: 1rem; + --ring:222.2 84% 4.9%; + --radius: 0.25rem; } .dark { @@ -70,7 +67,7 @@ $editorContainerId: 'editor-container'; --popover:222.2 84% 4.9%; --popover-foreground:210 40% 98%; - --primary:217.2 91.2% 59.8%; + --primary:210 40% 98%; --primary-foreground:222.2 47.4% 11.2%; --secondary:217.2 32.6% 17.5%; @@ -87,18 +84,21 @@ $editorContainerId: 'editor-container'; --border:217.2 32.6% 17.5%; --input:217.2 32.6% 17.5%; - --ring:224.3 76.3% 48%; + --ring:212.7 26.8% 83.9; } } -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} + + + +// @layer base { +// * { +// @apply border-border; +// } +// body { +// @apply bg-background text-foreground; +// } +// } // charts @layer base { @@ -164,10 +164,9 @@ $editorContainerId: 'editor-container'; pb-3 min-w-[30%] max-w-[70%] border - rounded-md - ; + rounded-xl; - box-shadow: 2px 2px 2px 0px rgba(0,0,0,0.1); + box-shadow: 1px 1px 1px 0px rgba(0, 0, 0, 0.1); // Making email tables fit. table { @@ -189,10 +188,10 @@ $editorContainerId: 'editor-container'; background: rgba(0, 0, 0, 0.6); } -[class$='gmail_quote'] { - display: none !important; -} +// [class$='gmail_quote'] { +// display: none !important; +// } -blockquote { - display: none !important; -} +// blockquote { +// display: none !important; +// } diff --git a/frontend/src/components/NavBar.vue b/frontend/src/components/NavBar.vue index 2331dbbb..f6ebcde2 100644 --- a/frontend/src/components/NavBar.vue +++ b/frontend/src/components/NavBar.vue @@ -19,7 +19,7 @@ defineProps({ const route = useRoute(); const getButtonVariant = (title) => { - return route.name === title.toLowerCase() ? "secondary" : "ghost" + return route.name === title.toLowerCase() ? "" : "ghost" } @@ -64,7 +64,7 @@ const getButtonVariant = (title) => { {{ link.title }} diff --git a/frontend/src/components/conversation/ConversationSideBar.vue b/frontend/src/components/conversation/ConversationSideBar.vue index cddcaee3..f8d04563 100644 --- a/frontend/src/components/conversation/ConversationSideBar.vue +++ b/frontend/src/components/conversation/ConversationSideBar.vue @@ -220,11 +220,17 @@ - - +
+

+ First reply at +

+

+ {{ format(conversationStore.conversation.data.first_reply_at, "PPpp") }} +

+

+ - +

+
diff --git a/frontend/src/components/conversation/ReplyBox.vue b/frontend/src/components/conversation/ReplyBox.vue index 3a264940..a95bf459 100644 --- a/frontend/src/components/conversation/ReplyBox.vue +++ b/frontend/src/components/conversation/ReplyBox.vue @@ -26,13 +26,13 @@ onMounted(() => { cannedResponsesStore.fetchAll() }) -const sendMessage = async (message) => { +const sendMessage = (message) => { api.sendMessage(conversationStore.conversation.data.uuid, { private: message.private, message: message.html, attachments: JSON.stringify(message.attachments), }) - await api.updateAssigneeLastSeen(conversationStore.conversation.data.uuid) + api.updateAssigneeLastSeen(conversationStore.conversation.data.uuid) } \ No newline at end of file diff --git a/frontend/src/components/conversationlist/ConversationList.vue b/frontend/src/components/conversationlist/ConversationList.vue index d7d9cf14..23ce267a 100644 --- a/frontend/src/components/conversationlist/ConversationList.vue +++ b/frontend/src/components/conversationlist/ConversationList.vue @@ -31,15 +31,16 @@ -
+ + -
+
diff --git a/frontend/src/components/conversationlist/ConversationListItem.vue b/frontend/src/components/conversationlist/ConversationListItem.vue index 0c68afeb..a8d078ae 100644 --- a/frontend/src/components/conversationlist/ConversationListItem.vue +++ b/frontend/src/components/conversationlist/ConversationListItem.vue @@ -19,7 +19,7 @@ {{ conversation.inbox_name }}

- {{ conversationStore.getContactFullName (conversation.uuid)}} + {{ conversationStore.getContactFullName(conversation.uuid) }}

@@ -30,8 +30,8 @@
-

- {{ conversation.last_message }} +

+ {{ conversation.last_message }}

@@ -50,7 +50,7 @@ import { useRouter } from 'vue-router' import { useConversationStore } from '@/stores/conversation' import { formatTime } from '@/utils/datetime' -import { Mail } from 'lucide-vue-next' +import { Mail, CheckCheck } from 'lucide-vue-next' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' const router = useRouter() diff --git a/frontend/src/components/dashboard/agent/CountCards.vue b/frontend/src/components/dashboard/agent/CountCards.vue new file mode 100644 index 00000000..dd28e9db --- /dev/null +++ b/frontend/src/components/dashboard/agent/CountCards.vue @@ -0,0 +1,34 @@ + + \ No newline at end of file diff --git a/frontend/src/components/message/AgentMessageBubble.vue b/frontend/src/components/message/AgentMessageBubble.vue index e2e62e21..751663dd 100644 --- a/frontend/src/components/message/AgentMessageBubble.vue +++ b/frontend/src/components/message/AgentMessageBubble.vue @@ -26,8 +26,9 @@
- {{ - message.status }} + +
@@ -68,7 +69,7 @@ import { TooltipTrigger } from '@/components/ui/tooltip' import { Spinner } from '@/components/ui/spinner' -import { RotateCcw } from 'lucide-vue-next'; +import { RotateCcw, CheckCheck } from 'lucide-vue-next'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import MessageAttachmentPreview from "@/components/attachment/MessageAttachmentPreview.vue" diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue index e24b0163..0920d299 100644 --- a/frontend/src/views/DashboardView.vue +++ b/frontend/src/views/DashboardView.vue @@ -1,56 +1,37 @@