mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-24 03:16:20 +00:00
Merge pull request #234 from abhinavxd/update-plus-addressing
feat: add support for toggling plus addressing for email inbox.
This commit is contained in:
+9
-8
@@ -152,9 +152,9 @@ func handleOAuthCallback(r *fastglue.Request) error {
|
||||
redirectURI := oauthData["redirect_uri"]
|
||||
clientID := oauthData["client_id"]
|
||||
clientSecret := oauthData["client_secret"]
|
||||
tenantID := oauthData["tenant_id"] // Empty string if not set
|
||||
flowType := oauthData["flow_type"] // "new_inbox" or "reconnect"
|
||||
inboxIDStr := oauthData["inbox_id"] // Inbox ID for reconnect flow
|
||||
tenantID := oauthData["tenant_id"] // Empty string if not set
|
||||
flowType := oauthData["flow_type"] // "new_inbox" or "reconnect"
|
||||
inboxIDStr := oauthData["inbox_id"] // Inbox ID for reconnect flow
|
||||
|
||||
// Validate provider matches URL parameter
|
||||
if storedProvider != provider {
|
||||
@@ -310,11 +310,12 @@ func handleOAuthCallback(r *fastglue.Request) error {
|
||||
|
||||
// Create inbox config
|
||||
config := imodels.Config{
|
||||
SMTP: []imodels.SMTPConfig{smtpConfig},
|
||||
IMAP: []imodels.IMAPConfig{imapConfig},
|
||||
From: userEmail,
|
||||
AuthType: imodels.AuthTypeOAuth2,
|
||||
OAuth: oauthConfig,
|
||||
SMTP: []imodels.SMTPConfig{smtpConfig},
|
||||
IMAP: []imodels.IMAPConfig{imapConfig},
|
||||
From: userEmail,
|
||||
AuthType: imodels.AuthTypeOAuth2,
|
||||
OAuth: oauthConfig,
|
||||
EnablePlusAddressing: true,
|
||||
}
|
||||
|
||||
configJSON, err := json.Marshal(config)
|
||||
|
||||
@@ -39,6 +39,7 @@ var migList = []migFunc{
|
||||
{"v0.8.5", migrations.V0_8_5},
|
||||
{"v0.9.1", migrations.V0_9_1},
|
||||
{"v0.10.0", migrations.V0_10_0},
|
||||
{"v1.0.1", migrations.V1_0_1},
|
||||
}
|
||||
|
||||
// upgrade upgrades the database to the current version by running SQL migration files
|
||||
|
||||
@@ -54,11 +54,25 @@
|
||||
<Switch :checked="componentField.modelValue" @update:checked="handleChange" />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
<p class="!mt-2 text-muted-foreground text-sm">
|
||||
<p class="!mt-2 text-muted-foreground text-xs">
|
||||
{{ $t('admin.inbox.csatSurveys.description_2') }}
|
||||
</p>
|
||||
</FormField>
|
||||
|
||||
<FormField v-if="showFormFields" v-slot="{ componentField, handleChange }" name="enable_plus_addressing">
|
||||
<FormItem class="flex flex-row items-center justify-between box p-4">
|
||||
<div class="space-y-0.5">
|
||||
<FormLabel class="text-base">{{ $t('admin.inbox.enablePlusAddressing') }}</FormLabel>
|
||||
<FormDescription>
|
||||
{{ $t('admin.inbox.enablePlusAddressing.description') }}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch :checked="componentField.modelValue" @update:checked="handleChange" />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-if="setupMethod" v-slot="{ componentField }" name="auth_type">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
@@ -729,6 +743,7 @@ const form = useForm({
|
||||
from: '',
|
||||
enabled: true,
|
||||
csat_enabled: false,
|
||||
enable_plus_addressing: true,
|
||||
auth_type: AUTH_TYPE_PASSWORD,
|
||||
imap: {
|
||||
host: 'imap.gmail.com',
|
||||
|
||||
@@ -7,6 +7,7 @@ export const createFormSchema = (t) => z.object({
|
||||
from: z.string().min(1, t('globals.messages.required')),
|
||||
enabled: z.boolean().optional(),
|
||||
csat_enabled: z.boolean().optional(),
|
||||
enable_plus_addressing: z.boolean().optional(),
|
||||
auth_type: z.enum([AUTH_TYPE_PASSWORD, AUTH_TYPE_OAUTH2]),
|
||||
oauth: z.object({
|
||||
access_token: z.string().optional(),
|
||||
|
||||
@@ -32,6 +32,7 @@ const submitForm = (values) => {
|
||||
// Prepare request payload from form values
|
||||
const config = {
|
||||
auth_type: values.auth_type,
|
||||
enable_plus_addressing: values.enable_plus_addressing,
|
||||
imap: [{ ...values.imap }],
|
||||
smtp: [{ ...values.smtp }]
|
||||
}
|
||||
@@ -106,6 +107,7 @@ onMounted(async () => {
|
||||
}
|
||||
inboxData.auth_type = inboxData?.config?.auth_type || AUTH_TYPE_PASSWORD
|
||||
inboxData.oauth = inboxData?.config?.oauth || {}
|
||||
inboxData.enable_plus_addressing = inboxData?.config?.enable_plus_addressing || false
|
||||
inbox.value = inboxData
|
||||
} catch (error) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
|
||||
@@ -158,6 +158,7 @@ const submitForm = (values) => {
|
||||
from: values.from,
|
||||
channel: channelName,
|
||||
config: {
|
||||
enable_plus_addressing: values.enable_plus_addressing,
|
||||
imap: [values.imap],
|
||||
smtp: [values.smtp]
|
||||
}
|
||||
|
||||
@@ -511,6 +511,8 @@
|
||||
"admin.inbox.heloHostname.description": "The hostname to use in the HELO/EHLO command. If not set, defaults to localhost.",
|
||||
"admin.inbox.skipTLSVerification": "Skip TLS Verification",
|
||||
"admin.inbox.skipTLSVerification.description": "Skip hostname check on the TLS certificate.",
|
||||
"admin.inbox.enablePlusAddressing": "Enable plus addressing",
|
||||
"admin.inbox.enablePlusAddressing.description": "Improves conversation threading but requires provider support (e.g., Gmail, Microsoft 365).",
|
||||
"admin.inbox.chooseChannel": "Choose a channel",
|
||||
"admin.inbox.configureChannel": "Configure channel",
|
||||
"admin.inbox.createEmailInbox": "Create Email Inbox",
|
||||
|
||||
@@ -33,6 +33,7 @@ type Email struct {
|
||||
headers map[string]string
|
||||
lo *logf.Logger
|
||||
from string
|
||||
enablePlusAddressing bool
|
||||
messageStore inbox.MessageStore
|
||||
userStore inbox.UserStore
|
||||
wg sync.WaitGroup
|
||||
@@ -77,6 +78,7 @@ func New(store inbox.MessageStore, userStore inbox.UserStore, opts Opts) (*Email
|
||||
userStore: userStore,
|
||||
oauth: opts.Config.OAuth,
|
||||
authType: opts.Config.AuthType,
|
||||
enablePlusAddressing: opts.Config.EnablePlusAddressing,
|
||||
tokenRefreshCallback: opts.TokenRefreshCallback,
|
||||
}
|
||||
return e, nil
|
||||
@@ -124,11 +126,12 @@ func (e *Email) getCurrentConfig() models.Config {
|
||||
e.oauthMu.RUnlock()
|
||||
|
||||
return models.Config{
|
||||
SMTP: e.smtpCfg,
|
||||
IMAP: e.imapCfg,
|
||||
From: e.from,
|
||||
OAuth: oauth,
|
||||
AuthType: e.authType,
|
||||
SMTP: e.smtpCfg,
|
||||
IMAP: e.imapCfg,
|
||||
From: e.from,
|
||||
OAuth: oauth,
|
||||
AuthType: e.authType,
|
||||
EnablePlusAddressing: e.enablePlusAddressing,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -180,9 +180,9 @@ func (e *Email) Send(m models.Message) error {
|
||||
}
|
||||
email.Headers.Set(headerLibredeskLoopPrevention, emailAddress)
|
||||
|
||||
// Set Reply-To with plus-addressing for conversation matching
|
||||
// Set Reply-To with plus-addressing for conversation matching (if enabled)
|
||||
// e.g., support@company.com → support+conv-{uuid}@company.com
|
||||
if m.ConversationUUID != "" {
|
||||
if e.enablePlusAddressing && m.ConversationUUID != "" {
|
||||
replyToAddr := buildPlusAddress(emailAddress, m.ConversationUUID)
|
||||
email.Headers.Set("Reply-To", replyToAddr)
|
||||
e.lo.Debug("Reply-To header set with plus-addressing", "reply_to", replyToAddr)
|
||||
|
||||
+22
-20
@@ -305,16 +305,18 @@ func (m *Manager) Update(id int, inbox imodels.Inbox) (imodels.Inbox, error) {
|
||||
switch current.Channel {
|
||||
case "email":
|
||||
var currentCfg struct {
|
||||
AuthType string `json:"auth_type"`
|
||||
OAuth map[string]string `json:"oauth"`
|
||||
IMAP []map[string]interface{} `json:"imap"`
|
||||
SMTP []map[string]interface{} `json:"smtp"`
|
||||
AuthType string `json:"auth_type"`
|
||||
OAuth map[string]string `json:"oauth"`
|
||||
IMAP []map[string]any `json:"imap"`
|
||||
SMTP []map[string]any `json:"smtp"`
|
||||
EnablePlusAddressing bool `json:"enable_plus_addressing"`
|
||||
}
|
||||
var updateCfg struct {
|
||||
AuthType string `json:"auth_type"`
|
||||
OAuth map[string]string `json:"oauth"`
|
||||
IMAP []map[string]interface{} `json:"imap"`
|
||||
SMTP []map[string]interface{} `json:"smtp"`
|
||||
AuthType string `json:"auth_type"`
|
||||
OAuth map[string]string `json:"oauth"`
|
||||
IMAP []map[string]any `json:"imap"`
|
||||
SMTP []map[string]any `json:"smtp"`
|
||||
EnablePlusAddressing bool `json:"enable_plus_addressing"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(current.Config, ¤tCfg); err != nil {
|
||||
@@ -496,15 +498,15 @@ func (m *Manager) encryptInboxConfig(config json.RawMessage) (json.RawMessage, e
|
||||
return config, nil
|
||||
}
|
||||
|
||||
var cfg map[string]interface{}
|
||||
var cfg map[string]any
|
||||
if err := json.Unmarshal(config, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("unmarshalling config: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt SMTP passwords
|
||||
if smtpSlice, ok := cfg["smtp"].([]interface{}); ok {
|
||||
if smtpSlice, ok := cfg["smtp"].([]any); ok {
|
||||
for i, smtpItem := range smtpSlice {
|
||||
if smtpMap, ok := smtpItem.(map[string]interface{}); ok {
|
||||
if smtpMap, ok := smtpItem.(map[string]any); ok {
|
||||
if password, ok := smtpMap["password"].(string); ok && password != "" {
|
||||
encrypted, err := crypto.Encrypt(password, m.encryptionKey)
|
||||
if err != nil {
|
||||
@@ -517,9 +519,9 @@ func (m *Manager) encryptInboxConfig(config json.RawMessage) (json.RawMessage, e
|
||||
}
|
||||
|
||||
// Encrypt IMAP passwords
|
||||
if imapSlice, ok := cfg["imap"].([]interface{}); ok {
|
||||
if imapSlice, ok := cfg["imap"].([]any); ok {
|
||||
for i, imapItem := range imapSlice {
|
||||
if imapMap, ok := imapItem.(map[string]interface{}); ok {
|
||||
if imapMap, ok := imapItem.(map[string]any); ok {
|
||||
if password, ok := imapMap["password"].(string); ok && password != "" {
|
||||
encrypted, err := crypto.Encrypt(password, m.encryptionKey)
|
||||
if err != nil {
|
||||
@@ -532,7 +534,7 @@ func (m *Manager) encryptInboxConfig(config json.RawMessage) (json.RawMessage, e
|
||||
}
|
||||
|
||||
// Encrypt OAuth fields if present
|
||||
if oauthMap, ok := cfg["oauth"].(map[string]interface{}); ok {
|
||||
if oauthMap, ok := cfg["oauth"].(map[string]any); ok {
|
||||
fields := []string{"client_secret", "access_token", "refresh_token"}
|
||||
for _, fieldName := range fields {
|
||||
if fieldValue, ok := oauthMap[fieldName].(string); ok && fieldValue != "" {
|
||||
@@ -559,15 +561,15 @@ func (m *Manager) decryptInboxConfig(config json.RawMessage) (json.RawMessage, e
|
||||
return config, nil
|
||||
}
|
||||
|
||||
var cfg map[string]interface{}
|
||||
var cfg map[string]any
|
||||
if err := json.Unmarshal(config, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("unmarshalling config: %w", err)
|
||||
}
|
||||
|
||||
// Decrypt SMTP passwords
|
||||
if smtpSlice, ok := cfg["smtp"].([]interface{}); ok {
|
||||
if smtpSlice, ok := cfg["smtp"].([]any); ok {
|
||||
for i, smtpItem := range smtpSlice {
|
||||
if smtpMap, ok := smtpItem.(map[string]interface{}); ok {
|
||||
if smtpMap, ok := smtpItem.(map[string]any); ok {
|
||||
if password, ok := smtpMap["password"].(string); ok && password != "" {
|
||||
decrypted, err := crypto.Decrypt(password, m.encryptionKey)
|
||||
if err != nil {
|
||||
@@ -580,9 +582,9 @@ func (m *Manager) decryptInboxConfig(config json.RawMessage) (json.RawMessage, e
|
||||
}
|
||||
|
||||
// Decrypt IMAP passwords
|
||||
if imapSlice, ok := cfg["imap"].([]interface{}); ok {
|
||||
if imapSlice, ok := cfg["imap"].([]any); ok {
|
||||
for i, imapItem := range imapSlice {
|
||||
if imapMap, ok := imapItem.(map[string]interface{}); ok {
|
||||
if imapMap, ok := imapItem.(map[string]any); ok {
|
||||
if password, ok := imapMap["password"].(string); ok && password != "" {
|
||||
decrypted, err := crypto.Decrypt(password, m.encryptionKey)
|
||||
if err != nil {
|
||||
@@ -595,7 +597,7 @@ func (m *Manager) decryptInboxConfig(config json.RawMessage) (json.RawMessage, e
|
||||
}
|
||||
|
||||
// Decrypt OAuth fields if present
|
||||
if oauthMap, ok := cfg["oauth"].(map[string]interface{}); ok {
|
||||
if oauthMap, ok := cfg["oauth"].(map[string]any); ok {
|
||||
fields := []string{"client_secret", "access_token", "refresh_token"}
|
||||
for _, fieldName := range fields {
|
||||
if fieldValue, ok := oauthMap[fieldName].(string); ok && fieldValue != "" {
|
||||
|
||||
@@ -31,11 +31,12 @@ type Inbox struct {
|
||||
|
||||
// Config holds the email inbox configuration with multiple SMTP servers and IMAP clients.
|
||||
type Config struct {
|
||||
AuthType string `json:"auth_type"` // AuthTypePassword or AuthTypeOAuth2
|
||||
OAuth *OAuthConfig `json:"oauth"` // OAuth config when auth_type is "oauth2"
|
||||
SMTP []SMTPConfig `json:"smtp"`
|
||||
IMAP []IMAPConfig `json:"imap"`
|
||||
From string `json:"from"`
|
||||
AuthType string `json:"auth_type"` // AuthTypePassword or AuthTypeOAuth2
|
||||
OAuth *OAuthConfig `json:"oauth"` // OAuth config when auth_type is "oauth2"
|
||||
SMTP []SMTPConfig `json:"smtp"`
|
||||
IMAP []IMAPConfig `json:"imap"`
|
||||
From string `json:"from"`
|
||||
EnablePlusAddressing bool `json:"enable_plus_addressing"` // Enable plus-addressing in Reply-To header for conversation matching
|
||||
}
|
||||
|
||||
// OAuthConfig holds OAuth 2.0 authentication details.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"github.com/knadh/stuffbin"
|
||||
)
|
||||
|
||||
// V1_0_1 updates the database schema to v1.0.1.
|
||||
func V1_0_1(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
|
||||
// Backfill enable_plus_addressing to true for existing email inboxes
|
||||
// that don't have this field in their config JSON.
|
||||
_, err := db.Exec(`
|
||||
UPDATE inboxes
|
||||
SET config = jsonb_set(config, '{enable_plus_addressing}', 'true'::jsonb, true)
|
||||
WHERE channel = 'email'
|
||||
AND NOT (config ? 'enable_plus_addressing');
|
||||
`)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user