Skip to content

Commit aeaae51

Browse files
authored
Merge pull request #234 from abhinavxd/update-plus-addressing
feat: add support for toggling plus addressing for email inbox.
2 parents 5a24fbe + 9ad46cc commit aeaae51

File tree

12 files changed

+90
-41
lines changed

12 files changed

+90
-41
lines changed

cmd/oauth.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,9 @@ func handleOAuthCallback(r *fastglue.Request) error {
152152
redirectURI := oauthData["redirect_uri"]
153153
clientID := oauthData["client_id"]
154154
clientSecret := oauthData["client_secret"]
155-
tenantID := oauthData["tenant_id"] // Empty string if not set
156-
flowType := oauthData["flow_type"] // "new_inbox" or "reconnect"
157-
inboxIDStr := oauthData["inbox_id"] // Inbox ID for reconnect flow
155+
tenantID := oauthData["tenant_id"] // Empty string if not set
156+
flowType := oauthData["flow_type"] // "new_inbox" or "reconnect"
157+
inboxIDStr := oauthData["inbox_id"] // Inbox ID for reconnect flow
158158

159159
// Validate provider matches URL parameter
160160
if storedProvider != provider {
@@ -310,11 +310,12 @@ func handleOAuthCallback(r *fastglue.Request) error {
310310

311311
// Create inbox config
312312
config := imodels.Config{
313-
SMTP: []imodels.SMTPConfig{smtpConfig},
314-
IMAP: []imodels.IMAPConfig{imapConfig},
315-
From: userEmail,
316-
AuthType: imodels.AuthTypeOAuth2,
317-
OAuth: oauthConfig,
313+
SMTP: []imodels.SMTPConfig{smtpConfig},
314+
IMAP: []imodels.IMAPConfig{imapConfig},
315+
From: userEmail,
316+
AuthType: imodels.AuthTypeOAuth2,
317+
OAuth: oauthConfig,
318+
EnablePlusAddressing: true,
318319
}
319320

320321
configJSON, err := json.Marshal(config)

cmd/upgrade.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ var migList = []migFunc{
3939
{"v0.8.5", migrations.V0_8_5},
4040
{"v0.9.1", migrations.V0_9_1},
4141
{"v0.10.0", migrations.V0_10_0},
42+
{"v1.0.1", migrations.V1_0_1},
4243
}
4344

4445
// upgrade upgrades the database to the current version by running SQL migration files

frontend/src/features/admin/inbox/EmailInboxForm.vue

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,25 @@
5454
<Switch :checked="componentField.modelValue" @update:checked="handleChange" />
5555
</FormControl>
5656
</FormItem>
57-
<p class="!mt-2 text-muted-foreground text-sm">
57+
<p class="!mt-2 text-muted-foreground text-xs">
5858
{{ $t('admin.inbox.csatSurveys.description_2') }}
5959
</p>
6060
</FormField>
6161

62+
<FormField v-if="showFormFields" v-slot="{ componentField, handleChange }" name="enable_plus_addressing">
63+
<FormItem class="flex flex-row items-center justify-between box p-4">
64+
<div class="space-y-0.5">
65+
<FormLabel class="text-base">{{ $t('admin.inbox.enablePlusAddressing') }}</FormLabel>
66+
<FormDescription>
67+
{{ $t('admin.inbox.enablePlusAddressing.description') }}
68+
</FormDescription>
69+
</div>
70+
<FormControl>
71+
<Switch :checked="componentField.modelValue" @update:checked="handleChange" />
72+
</FormControl>
73+
</FormItem>
74+
</FormField>
75+
6276
<FormField v-if="setupMethod" v-slot="{ componentField }" name="auth_type">
6377
<FormItem>
6478
<FormControl>
@@ -729,6 +743,7 @@ const form = useForm({
729743
from: '',
730744
enabled: true,
731745
csat_enabled: false,
746+
enable_plus_addressing: true,
732747
auth_type: AUTH_TYPE_PASSWORD,
733748
imap: {
734749
host: 'imap.gmail.com',

frontend/src/features/admin/inbox/formSchema.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const createFormSchema = (t) => z.object({
77
from: z.string().min(1, t('globals.messages.required')),
88
enabled: z.boolean().optional(),
99
csat_enabled: z.boolean().optional(),
10+
enable_plus_addressing: z.boolean().optional(),
1011
auth_type: z.enum([AUTH_TYPE_PASSWORD, AUTH_TYPE_OAUTH2]),
1112
oauth: z.object({
1213
access_token: z.string().optional(),

frontend/src/views/admin/inbox/EditInbox.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const submitForm = (values) => {
3232
// Prepare request payload from form values
3333
const config = {
3434
auth_type: values.auth_type,
35+
enable_plus_addressing: values.enable_plus_addressing,
3536
imap: [{ ...values.imap }],
3637
smtp: [{ ...values.smtp }]
3738
}
@@ -106,6 +107,7 @@ onMounted(async () => {
106107
}
107108
inboxData.auth_type = inboxData?.config?.auth_type || AUTH_TYPE_PASSWORD
108109
inboxData.oauth = inboxData?.config?.oauth || {}
110+
inboxData.enable_plus_addressing = inboxData?.config?.enable_plus_addressing || false
109111
inbox.value = inboxData
110112
} catch (error) {
111113
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {

frontend/src/views/admin/inbox/NewInbox.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ const submitForm = (values) => {
158158
from: values.from,
159159
channel: channelName,
160160
config: {
161+
enable_plus_addressing: values.enable_plus_addressing,
161162
imap: [values.imap],
162163
smtp: [values.smtp]
163164
}

i18n/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,8 @@
511511
"admin.inbox.heloHostname.description": "The hostname to use in the HELO/EHLO command. If not set, defaults to localhost.",
512512
"admin.inbox.skipTLSVerification": "Skip TLS Verification",
513513
"admin.inbox.skipTLSVerification.description": "Skip hostname check on the TLS certificate.",
514+
"admin.inbox.enablePlusAddressing": "Enable plus addressing",
515+
"admin.inbox.enablePlusAddressing.description": "Improves conversation threading but requires provider support (e.g., Gmail, Microsoft 365).",
514516
"admin.inbox.chooseChannel": "Choose a channel",
515517
"admin.inbox.configureChannel": "Configure channel",
516518
"admin.inbox.createEmailInbox": "Create Email Inbox",

internal/inbox/channel/email/email.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type Email struct {
3333
headers map[string]string
3434
lo *logf.Logger
3535
from string
36+
enablePlusAddressing bool
3637
messageStore inbox.MessageStore
3738
userStore inbox.UserStore
3839
wg sync.WaitGroup
@@ -77,6 +78,7 @@ func New(store inbox.MessageStore, userStore inbox.UserStore, opts Opts) (*Email
7778
userStore: userStore,
7879
oauth: opts.Config.OAuth,
7980
authType: opts.Config.AuthType,
81+
enablePlusAddressing: opts.Config.EnablePlusAddressing,
8082
tokenRefreshCallback: opts.TokenRefreshCallback,
8183
}
8284
return e, nil
@@ -124,11 +126,12 @@ func (e *Email) getCurrentConfig() models.Config {
124126
e.oauthMu.RUnlock()
125127

126128
return models.Config{
127-
SMTP: e.smtpCfg,
128-
IMAP: e.imapCfg,
129-
From: e.from,
130-
OAuth: oauth,
131-
AuthType: e.authType,
129+
SMTP: e.smtpCfg,
130+
IMAP: e.imapCfg,
131+
From: e.from,
132+
OAuth: oauth,
133+
AuthType: e.authType,
134+
EnablePlusAddressing: e.enablePlusAddressing,
132135
}
133136
}
134137

internal/inbox/channel/email/smtp.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,9 @@ func (e *Email) Send(m models.Message) error {
180180
}
181181
email.Headers.Set(headerLibredeskLoopPrevention, emailAddress)
182182

183-
// Set Reply-To with plus-addressing for conversation matching
183+
// Set Reply-To with plus-addressing for conversation matching (if enabled)
184184
// e.g., support@company.com → support+conv-{uuid}@company.com
185-
if m.ConversationUUID != "" {
185+
if e.enablePlusAddressing && m.ConversationUUID != "" {
186186
replyToAddr := buildPlusAddress(emailAddress, m.ConversationUUID)
187187
email.Headers.Set("Reply-To", replyToAddr)
188188
e.lo.Debug("Reply-To header set with plus-addressing", "reply_to", replyToAddr)

internal/inbox/inbox.go

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -305,16 +305,18 @@ func (m *Manager) Update(id int, inbox imodels.Inbox) (imodels.Inbox, error) {
305305
switch current.Channel {
306306
case "email":
307307
var currentCfg struct {
308-
AuthType string `json:"auth_type"`
309-
OAuth map[string]string `json:"oauth"`
310-
IMAP []map[string]interface{} `json:"imap"`
311-
SMTP []map[string]interface{} `json:"smtp"`
308+
AuthType string `json:"auth_type"`
309+
OAuth map[string]string `json:"oauth"`
310+
IMAP []map[string]any `json:"imap"`
311+
SMTP []map[string]any `json:"smtp"`
312+
EnablePlusAddressing bool `json:"enable_plus_addressing"`
312313
}
313314
var updateCfg struct {
314-
AuthType string `json:"auth_type"`
315-
OAuth map[string]string `json:"oauth"`
316-
IMAP []map[string]interface{} `json:"imap"`
317-
SMTP []map[string]interface{} `json:"smtp"`
315+
AuthType string `json:"auth_type"`
316+
OAuth map[string]string `json:"oauth"`
317+
IMAP []map[string]any `json:"imap"`
318+
SMTP []map[string]any `json:"smtp"`
319+
EnablePlusAddressing bool `json:"enable_plus_addressing"`
318320
}
319321

320322
if err := json.Unmarshal(current.Config, &currentCfg); err != nil {
@@ -496,15 +498,15 @@ func (m *Manager) encryptInboxConfig(config json.RawMessage) (json.RawMessage, e
496498
return config, nil
497499
}
498500

499-
var cfg map[string]interface{}
501+
var cfg map[string]any
500502
if err := json.Unmarshal(config, &cfg); err != nil {
501503
return nil, fmt.Errorf("unmarshalling config: %w", err)
502504
}
503505

504506
// Encrypt SMTP passwords
505-
if smtpSlice, ok := cfg["smtp"].([]interface{}); ok {
507+
if smtpSlice, ok := cfg["smtp"].([]any); ok {
506508
for i, smtpItem := range smtpSlice {
507-
if smtpMap, ok := smtpItem.(map[string]interface{}); ok {
509+
if smtpMap, ok := smtpItem.(map[string]any); ok {
508510
if password, ok := smtpMap["password"].(string); ok && password != "" {
509511
encrypted, err := crypto.Encrypt(password, m.encryptionKey)
510512
if err != nil {
@@ -517,9 +519,9 @@ func (m *Manager) encryptInboxConfig(config json.RawMessage) (json.RawMessage, e
517519
}
518520

519521
// Encrypt IMAP passwords
520-
if imapSlice, ok := cfg["imap"].([]interface{}); ok {
522+
if imapSlice, ok := cfg["imap"].([]any); ok {
521523
for i, imapItem := range imapSlice {
522-
if imapMap, ok := imapItem.(map[string]interface{}); ok {
524+
if imapMap, ok := imapItem.(map[string]any); ok {
523525
if password, ok := imapMap["password"].(string); ok && password != "" {
524526
encrypted, err := crypto.Encrypt(password, m.encryptionKey)
525527
if err != nil {
@@ -532,7 +534,7 @@ func (m *Manager) encryptInboxConfig(config json.RawMessage) (json.RawMessage, e
532534
}
533535

534536
// Encrypt OAuth fields if present
535-
if oauthMap, ok := cfg["oauth"].(map[string]interface{}); ok {
537+
if oauthMap, ok := cfg["oauth"].(map[string]any); ok {
536538
fields := []string{"client_secret", "access_token", "refresh_token"}
537539
for _, fieldName := range fields {
538540
if fieldValue, ok := oauthMap[fieldName].(string); ok && fieldValue != "" {
@@ -559,15 +561,15 @@ func (m *Manager) decryptInboxConfig(config json.RawMessage) (json.RawMessage, e
559561
return config, nil
560562
}
561563

562-
var cfg map[string]interface{}
564+
var cfg map[string]any
563565
if err := json.Unmarshal(config, &cfg); err != nil {
564566
return nil, fmt.Errorf("unmarshalling config: %w", err)
565567
}
566568

567569
// Decrypt SMTP passwords
568-
if smtpSlice, ok := cfg["smtp"].([]interface{}); ok {
570+
if smtpSlice, ok := cfg["smtp"].([]any); ok {
569571
for i, smtpItem := range smtpSlice {
570-
if smtpMap, ok := smtpItem.(map[string]interface{}); ok {
572+
if smtpMap, ok := smtpItem.(map[string]any); ok {
571573
if password, ok := smtpMap["password"].(string); ok && password != "" {
572574
decrypted, err := crypto.Decrypt(password, m.encryptionKey)
573575
if err != nil {
@@ -580,9 +582,9 @@ func (m *Manager) decryptInboxConfig(config json.RawMessage) (json.RawMessage, e
580582
}
581583

582584
// Decrypt IMAP passwords
583-
if imapSlice, ok := cfg["imap"].([]interface{}); ok {
585+
if imapSlice, ok := cfg["imap"].([]any); ok {
584586
for i, imapItem := range imapSlice {
585-
if imapMap, ok := imapItem.(map[string]interface{}); ok {
587+
if imapMap, ok := imapItem.(map[string]any); ok {
586588
if password, ok := imapMap["password"].(string); ok && password != "" {
587589
decrypted, err := crypto.Decrypt(password, m.encryptionKey)
588590
if err != nil {
@@ -595,7 +597,7 @@ func (m *Manager) decryptInboxConfig(config json.RawMessage) (json.RawMessage, e
595597
}
596598

597599
// Decrypt OAuth fields if present
598-
if oauthMap, ok := cfg["oauth"].(map[string]interface{}); ok {
600+
if oauthMap, ok := cfg["oauth"].(map[string]any); ok {
599601
fields := []string{"client_secret", "access_token", "refresh_token"}
600602
for _, fieldName := range fields {
601603
if fieldValue, ok := oauthMap[fieldName].(string); ok && fieldValue != "" {

0 commit comments

Comments
 (0)