feat: add personal mailbox forwarding

This commit is contained in:
zxyszx
2026-08-02 07:37:07 +08:00
parent 77a0fd254c
commit 7f05a70f60
11 changed files with 683 additions and 46 deletions
+20
View File
@@ -227,6 +227,26 @@ func (a *App) migrate(ctx context.Context) error {
updated_at TEXT NOT NULL,
UNIQUE(domain_id, local_part)
)`,
`CREATE TABLE IF NOT EXISTS forwarding_verified_emails (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
email TEXT NOT NULL,
verified INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(user_id, email)
)`,
`CREATE INDEX IF NOT EXISTS idx_forwarding_verified_emails_user ON forwarding_verified_emails(user_id, email)`,
`CREATE TABLE IF NOT EXISTS account_forwarding_settings (
user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
target_email TEXT NOT NULL DEFAULT '',
updated_at TEXT NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS mailbox_forwarding_settings (
mailbox_id TEXT PRIMARY KEY REFERENCES mailboxes(id) ON DELETE CASCADE,
target_email TEXT NOT NULL DEFAULT '',
updated_at TEXT NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS aliases (
id TEXT PRIMARY KEY,
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
+100
View File
@@ -1642,6 +1642,106 @@ func TestMailSendQueuesSMTPFailureForRetry(t *testing.T) {
}
}
func TestInboundForwardingSettingsAndDelivery(t *testing.T) {
a := newTestApp(t)
stopTestWorkers(a)
host, port, received := startCapturingSMTP(t, 2)
a.cfg.SMTPHost = host
a.cfg.SMTPPort = port
ts := httptest.NewServer(a.Router())
defer ts.Close()
admin := &testClient{t: t, server: ts}
var login map[string]any
if code := admin.do("POST", "/api/auth/login", map[string]string{"email": "admin@lanqin.local", "password": "ChangeMe123!"}, &login); code != http.StatusOK {
t.Fatalf("login code=%d body=%v", code, login)
}
_, mb := defaultAdminUserAndMailbox(t, a)
var settings ForwardingSettings
if code := admin.do("POST", "/api/me/forwarding/verified-emails", map[string]string{"email": "account-forward@example.test"}, &settings); code != http.StatusCreated {
t.Fatalf("add account forwarding target code=%d settings=%+v", code, settings)
}
if code := admin.do("POST", "/api/me/forwarding/account", map[string]string{"targetEmail": "account-forward@example.test"}, &settings); code != http.StatusOK || settings.AccountTargetEmail != "account-forward@example.test" {
t.Fatalf("save account forwarding code=%d settings=%+v", code, settings)
}
ctx := context.Background()
inboxID, err := a.ensureFolder(ctx, mb.ID, "Inbox")
if err != nil {
t.Fatal(err)
}
insertInbound := func(messageID, subject string, raw []byte) string {
t.Helper()
now := a.now().UTC()
id, err := a.insertMessage(ctx, storedMessage{
MailboxID: mb.ID,
FolderID: inboxID,
MessageUID: newID("uid"),
MessageID: messageID,
Subject: subject,
From: "sender@example.test",
To: []string{mb.Address},
SentAt: now,
ReceivedAt: now,
Snippet: "body",
BodyText: "body",
IsRead: false,
RecipientAddr: mb.Address,
}, nil)
if err != nil {
t.Fatal(err)
}
a.processInboundForwarding(ctx, id, mb.ID, raw)
return id
}
raw := []byte("From: sender@example.test\r\nTo: admin@lanqin.local\r\nSubject: account forward\r\nMessage-ID: <account-forward@example.test>\r\n\r\nbody")
firstID := insertInbound("<account-forward@example.test>", "account forward", raw)
var recipientsJSON string
if err := a.db.QueryRow(`SELECT recipients_json FROM send_queue WHERE source=? AND sent_message_id=?`, sendSourceForwarding, firstID).Scan(&recipientsJSON); err != nil {
t.Fatal(err)
}
if !strings.Contains(recipientsJSON, "account-forward@example.test") {
t.Fatalf("account forwarding recipients=%s", recipientsJSON)
}
if err := a.processDueSendQueue(ctx); err != nil {
t.Fatal(err)
}
select {
case body := <-received:
if !strings.Contains(body, forwardingHeaderName+": mail.example.test") || !strings.Contains(body, "X-LanQin-Forwarded-For: admin@lanqin.local") || strings.Contains(body, "\r\n\r\n\r\nbody") {
t.Fatalf("unexpected forwarded body: %q", body)
}
case <-time.After(2 * time.Second):
t.Fatal("account forwarding mail was not relayed")
}
if code := admin.do("POST", "/api/me/forwarding/verified-emails", map[string]string{"email": "mailbox-forward@example.test"}, &settings); code != http.StatusCreated {
t.Fatalf("add mailbox forwarding target code=%d settings=%+v", code, settings)
}
if code := admin.do("POST", "/api/me/mailboxes/"+mb.ID+"/forwarding", map[string]string{"targetEmail": "mailbox-forward@example.test"}, &settings); code != http.StatusOK {
t.Fatalf("save mailbox forwarding code=%d settings=%+v", code, settings)
}
raw = []byte("From: sender@example.test\r\nTo: admin@lanqin.local\r\nSubject: mailbox forward\r\nMessage-ID: <mailbox-forward@example.test>\r\n\r\nbody")
secondID := insertInbound("<mailbox-forward@example.test>", "mailbox forward", raw)
if err := a.db.QueryRow(`SELECT recipients_json FROM send_queue WHERE source=? AND sent_message_id=?`, sendSourceForwarding, secondID).Scan(&recipientsJSON); err != nil {
t.Fatal(err)
}
if !strings.Contains(recipientsJSON, "mailbox-forward@example.test") || strings.Contains(recipientsJSON, "account-forward@example.test") {
t.Fatalf("mailbox forwarding should override account target, recipients=%s", recipientsJSON)
}
loopRaw := []byte("From: sender@example.test\r\nTo: admin@lanqin.local\r\nSubject: loop\r\n" + forwardingHeaderName + ": mail.example.test\r\nMessage-ID: <forward-loop@example.test>\r\n\r\nbody")
insertInbound("<forward-loop@example.test>", "loop", loopRaw)
var queueCount int
if err := a.db.QueryRow(`SELECT COUNT(1) FROM send_queue WHERE source=?`, sendSourceForwarding).Scan(&queueCount); err != nil {
t.Fatal(err)
}
if queueCount != 2 {
t.Fatalf("forwarding queue count=%d, want 2", queueCount)
}
}
func TestMailSendRejectsUnauthorizedFrom(t *testing.T) {
a := newTestApp(t)
ts := httptest.NewServer(a.Router())
@@ -0,0 +1,153 @@
package app
import (
"bytes"
"context"
"fmt"
"os"
"strings"
)
const forwardingHeaderName = "X-LanQin-Forwarded-By"
func (a *App) processInboundForwarding(ctx context.Context, messageID, mailboxID string, raw []byte) {
target, userID, mailboxAddress, err := a.inboundForwardingTarget(ctx, mailboxID)
if err != nil {
a.log.Warn("failed to load forwarding target", "message", messageID, "mailbox", mailboxID, "error", err)
return
}
if target == "" || userID == "" || mailboxAddress == "" {
return
}
if normalizeEmail(target) == normalizeEmail(mailboxAddress) {
return
}
if len(raw) == 0 {
raw, err = a.forwardingRawMessage(ctx, messageID)
if err != nil {
a.log.Warn("failed to load raw message for forwarding", "message", messageID, "error", err)
return
}
}
if hasForwardingHeader(raw) {
a.log.Warn("skip forwarding message that already has LanQin forwarding header", "message", messageID, "mailbox", mailboxID)
return
}
forwarded := addForwardingHeaders(raw, mailboxAddress, a.cfg.PublicHostname)
var rfcMessageID string
_ = a.db.QueryRowContext(ctx, `SELECT message_id FROM messages WHERE id=?`, messageID).Scan(&rfcMessageID)
if strings.TrimSpace(rfcMessageID) == "" {
rfcMessageID = messageID
}
queueID, err := a.enqueueSend(ctx, sendQueueInput{
UserID: userID,
MailboxID: mailboxID,
SentMessageID: messageID,
MessageID: rfcMessageID,
Source: sendSourceForwarding,
MailFrom: mailboxAddress,
HeaderFrom: mailboxAddress,
Recipients: []string{target},
MIMEBytes: forwarded,
Now: a.now().UTC(),
})
if err != nil {
a.log.Warn("failed to enqueue inbound forwarding", "message", messageID, "mailbox", mailboxID, "target", target, "error", err)
return
}
if queueID == "" {
a.log.Warn("forwarding target configured but SMTP sending is not configured", "message", messageID, "mailbox", mailboxID, "target", target)
}
}
func (a *App) inboundForwardingTarget(ctx context.Context, mailboxID string) (targetEmail, userID, mailboxAddress string, err error) {
var mailboxTarget, accountTarget string
err = a.db.QueryRowContext(ctx, `SELECT mb.user_id,mb.address,COALESCE(mfs.target_email,''),COALESCE(afs.target_email,'')
FROM mailboxes mb
LEFT JOIN mailbox_forwarding_settings mfs ON mfs.mailbox_id=mb.id
LEFT JOIN account_forwarding_settings afs ON afs.user_id=mb.user_id
WHERE mb.id=? AND mb.status='active'`, mailboxID).Scan(&userID, &mailboxAddress, &mailboxTarget, &accountTarget)
if err != nil {
return "", "", "", err
}
target := normalizeEmail(mailboxTarget)
if target == "" {
target = normalizeEmail(accountTarget)
}
if target == "" {
return "", userID, mailboxAddress, nil
}
verified, err := a.forwardingEmailVerified(ctx, userID, target)
if err != nil {
return "", "", "", err
}
if !verified {
return "", userID, mailboxAddress, nil
}
return target, userID, mailboxAddress, nil
}
func (a *App) forwardingRawMessage(ctx context.Context, messageID string) ([]byte, error) {
msg, err := a.storedMessageByID(ctx, messageID)
if err != nil {
return nil, err
}
if strings.TrimSpace(msg.RawPath) != "" {
if ok, err := a.pathIsUnderMaildirRoot(msg.RawPath); err == nil && ok {
if raw, err := os.ReadFile(msg.RawPath); err == nil {
return raw, nil
}
}
}
attachments, err := a.attachmentInputsForMessage(ctx, messageID)
if err != nil {
return nil, err
}
return BuildMIME(MIMEMessage{
From: msg.From,
FromName: msg.FromName,
To: msg.To,
CC: msg.CC,
BCC: msg.BCC,
Subject: msg.Subject,
Text: msg.BodyText,
HTML: msg.BodyHTML,
MessageID: msg.MessageID,
Date: messageDate(msg),
Attachments: attachments,
})
}
func hasForwardingHeader(raw []byte) bool {
header := raw
if idx := bytes.Index(raw, []byte("\r\n\r\n")); idx >= 0 {
header = raw[:idx]
} else if idx := bytes.Index(raw, []byte("\n\n")); idx >= 0 {
header = raw[:idx]
}
return strings.Contains(strings.ToLower(string(header)), strings.ToLower(forwardingHeaderName)+":")
}
func addForwardingHeaders(raw []byte, mailboxAddress, hostname string) []byte {
hostname = strings.TrimSpace(hostname)
if hostname == "" {
hostname = "lanqin.local"
}
header := fmt.Sprintf("%s: %s\r\nX-LanQin-Forwarded-For: %s\r\n", forwardingHeaderName, hostname, normalizeEmail(mailboxAddress))
if idx := bytes.Index(raw, []byte("\r\n\r\n")); idx >= 0 {
out := make([]byte, 0, len(raw)+len(header))
out = append(out, raw[:idx]...)
out = append(out, []byte("\r\n"+header)...)
out = append(out, raw[idx+2:]...)
return out
}
if idx := bytes.Index(raw, []byte("\n\n")); idx >= 0 {
lfHeader := strings.ReplaceAll(header, "\r\n", "\n")
out := make([]byte, 0, len(raw)+len(lfHeader))
out = append(out, raw[:idx]...)
out = append(out, []byte("\n"+lfHeader)...)
out = append(out, raw[idx+1:]...)
return out
}
return append([]byte(header+"\r\n"), raw...)
}
@@ -0,0 +1,296 @@
package app
import (
"context"
"database/sql"
"errors"
"net/http"
"strings"
"time"
"github.com/go-chi/chi/v5"
)
type ForwardingVerifiedEmail struct {
ID string `json:"id"`
Email string `json:"email"`
Verified bool `json:"verified"`
CreatedAt time.Time `json:"createdAt"`
}
type MailboxForwardingRule struct {
MailboxID string `json:"mailboxId"`
TargetEmail string `json:"targetEmail"`
}
type ForwardingSettings struct {
VerifiedEmails []ForwardingVerifiedEmail `json:"verifiedEmails"`
AccountTargetEmail string `json:"accountTargetEmail"`
MailboxRules []MailboxForwardingRule `json:"mailboxRules"`
}
func (a *App) handleForwardingSettings(w http.ResponseWriter, r *http.Request) {
user := currentUser(r)
settings, err := a.forwardingSettings(r.Context(), user.ID)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to load forwarding settings")
return
}
respondJSON(w, http.StatusOK, settings)
}
func (a *App) handleAddForwardingVerifiedEmail(w http.ResponseWriter, r *http.Request) {
user := currentUser(r)
var req struct {
Email string `json:"email"`
}
if err := decodeJSON(r, &req); err != nil {
badRequest(w, err)
return
}
email := normalizeEmail(req.Email)
if email == "" || !strings.Contains(email, "@") {
badRequest(w, errors.New("邮箱地址无效"))
return
}
if owns, err := a.userOwnsMailboxAddress(r.Context(), user.ID, email); err != nil {
respondError(w, http.StatusInternalServerError, "failed to check mailbox")
return
} else if owns {
badRequest(w, errors.New("不能把当前账号邮箱作为转发验证邮箱"))
return
}
now := a.now().UTC().Format(time.RFC3339Nano)
id := newID("fwd")
_, err := a.db.ExecContext(r.Context(), `INSERT INTO forwarding_verified_emails(id,user_id,email,verified,created_at,updated_at)
VALUES(?,?,?,?,?,?)
ON CONFLICT(user_id,email) DO UPDATE SET verified=1,updated_at=excluded.updated_at`,
id, user.ID, email, 1, now, now)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to save verified email")
return
}
settings, err := a.forwardingSettings(r.Context(), user.ID)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to load forwarding settings")
return
}
respondJSON(w, http.StatusCreated, settings)
}
func (a *App) handleDeleteForwardingVerifiedEmail(w http.ResponseWriter, r *http.Request) {
user := currentUser(r)
id := strings.TrimSpace(chi.URLParam(r, "id"))
if id == "" {
respondError(w, http.StatusNotFound, "verified email not found")
return
}
var email string
if err := a.db.QueryRowContext(r.Context(), `SELECT email FROM forwarding_verified_emails WHERE id=? AND user_id=?`, id, user.ID).Scan(&email); err != nil {
respondError(w, http.StatusNotFound, "verified email not found")
return
}
tx, err := a.db.BeginTx(r.Context(), nil)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to start transaction")
return
}
defer tx.Rollback()
now := a.now().UTC().Format(time.RFC3339Nano)
if _, err := tx.ExecContext(r.Context(), `DELETE FROM forwarding_verified_emails WHERE id=? AND user_id=?`, id, user.ID); err != nil {
respondError(w, http.StatusInternalServerError, "failed to delete verified email")
return
}
if _, err := tx.ExecContext(r.Context(), `UPDATE account_forwarding_settings SET target_email='',updated_at=? WHERE user_id=? AND target_email=?`, now, user.ID, email); err != nil {
respondError(w, http.StatusInternalServerError, "failed to update account forwarding")
return
}
if _, err := tx.ExecContext(r.Context(), `DELETE FROM mailbox_forwarding_settings
WHERE target_email=? AND mailbox_id IN (SELECT id FROM mailboxes WHERE user_id=?)`, email, user.ID); err != nil {
respondError(w, http.StatusInternalServerError, "failed to update mailbox forwarding")
return
}
if err := tx.Commit(); err != nil {
respondError(w, http.StatusInternalServerError, "failed to save forwarding settings")
return
}
settings, err := a.forwardingSettings(r.Context(), user.ID)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to load forwarding settings")
return
}
respondJSON(w, http.StatusOK, settings)
}
func (a *App) handleUpdateAccountForwarding(w http.ResponseWriter, r *http.Request) {
user := currentUser(r)
var req struct {
TargetEmail string `json:"targetEmail"`
}
if err := decodeJSON(r, &req); err != nil {
badRequest(w, err)
return
}
target, err := a.cleanForwardingTarget(r.Context(), user.ID, req.TargetEmail)
if err != nil {
badRequest(w, err)
return
}
now := a.now().UTC().Format(time.RFC3339Nano)
_, err = a.db.ExecContext(r.Context(), `INSERT INTO account_forwarding_settings(user_id,target_email,updated_at)
VALUES(?,?,?)
ON CONFLICT(user_id) DO UPDATE SET target_email=excluded.target_email,updated_at=excluded.updated_at`,
user.ID, target, now)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to save account forwarding")
return
}
settings, err := a.forwardingSettings(r.Context(), user.ID)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to load forwarding settings")
return
}
respondJSON(w, http.StatusOK, settings)
}
func (a *App) handleUpdateMailboxForwarding(w http.ResponseWriter, r *http.Request) {
user := currentUser(r)
mailboxID := strings.TrimSpace(chi.URLParam(r, "id"))
if mailboxID == "" {
respondError(w, http.StatusNotFound, "mailbox not found")
return
}
if ok, err := a.userOwnsMailboxID(r.Context(), user.ID, mailboxID); err != nil {
respondError(w, http.StatusInternalServerError, "failed to check mailbox")
return
} else if !ok {
respondError(w, http.StatusNotFound, "mailbox not found")
return
}
var req struct {
TargetEmail string `json:"targetEmail"`
}
if err := decodeJSON(r, &req); err != nil {
badRequest(w, err)
return
}
target, err := a.cleanForwardingTarget(r.Context(), user.ID, req.TargetEmail)
if err != nil {
badRequest(w, err)
return
}
if target == "" {
if _, err := a.db.ExecContext(r.Context(), `DELETE FROM mailbox_forwarding_settings WHERE mailbox_id=?`, mailboxID); err != nil {
respondError(w, http.StatusInternalServerError, "failed to save mailbox forwarding")
return
}
} else {
now := a.now().UTC().Format(time.RFC3339Nano)
if _, err := a.db.ExecContext(r.Context(), `INSERT INTO mailbox_forwarding_settings(mailbox_id,target_email,updated_at)
VALUES(?,?,?)
ON CONFLICT(mailbox_id) DO UPDATE SET target_email=excluded.target_email,updated_at=excluded.updated_at`,
mailboxID, target, now); err != nil {
respondError(w, http.StatusInternalServerError, "failed to save mailbox forwarding")
return
}
}
settings, err := a.forwardingSettings(r.Context(), user.ID)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to load forwarding settings")
return
}
respondJSON(w, http.StatusOK, settings)
}
func (a *App) forwardingSettings(ctx context.Context, userID string) (ForwardingSettings, error) {
settings := ForwardingSettings{
VerifiedEmails: []ForwardingVerifiedEmail{},
MailboxRules: []MailboxForwardingRule{},
}
rows, err := a.db.QueryContext(ctx, `SELECT id,email,verified,created_at FROM forwarding_verified_emails WHERE user_id=? ORDER BY created_at DESC,email`, userID)
if err != nil {
return settings, err
}
defer rows.Close()
for rows.Next() {
var item ForwardingVerifiedEmail
var verified int
var created string
if err := rows.Scan(&item.ID, &item.Email, &verified, &created); err != nil {
return settings, err
}
item.Verified = intBool(verified)
item.CreatedAt = parseTime(created)
settings.VerifiedEmails = append(settings.VerifiedEmails, item)
}
if err := rows.Err(); err != nil {
return settings, err
}
err = a.db.QueryRowContext(ctx, `SELECT target_email FROM account_forwarding_settings WHERE user_id=?`, userID).Scan(&settings.AccountTargetEmail)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return settings, err
}
rows, err = a.db.QueryContext(ctx, `SELECT mfs.mailbox_id,mfs.target_email
FROM mailbox_forwarding_settings mfs
JOIN mailboxes mb ON mb.id=mfs.mailbox_id
WHERE mb.user_id=? AND mfs.target_email<>''
ORDER BY mb.address`, userID)
if err != nil {
return settings, err
}
defer rows.Close()
for rows.Next() {
var item MailboxForwardingRule
if err := rows.Scan(&item.MailboxID, &item.TargetEmail); err != nil {
return settings, err
}
settings.MailboxRules = append(settings.MailboxRules, item)
}
return settings, rows.Err()
}
func (a *App) cleanForwardingTarget(ctx context.Context, userID, value string) (string, error) {
value = strings.TrimSpace(value)
if value == "" || strings.EqualFold(value, "none") {
return "", nil
}
target := normalizeEmail(value)
if target == "" || !strings.Contains(target, "@") {
return "", errors.New("转发邮箱无效")
}
ok, err := a.forwardingEmailVerified(ctx, userID, target)
if err != nil {
return "", err
}
if !ok {
return "", errors.New("请先添加验证邮箱")
}
return target, nil
}
func (a *App) forwardingEmailVerified(ctx context.Context, userID, email string) (bool, error) {
var count int
err := a.db.QueryRowContext(ctx, `SELECT COUNT(1) FROM forwarding_verified_emails WHERE user_id=? AND email=? AND verified=1`, userID, normalizeEmail(email)).Scan(&count)
if err != nil {
return false, err
}
return count > 0, nil
}
func (a *App) userOwnsMailboxID(ctx context.Context, userID, mailboxID string) (bool, error) {
var count int
err := a.db.QueryRowContext(ctx, `SELECT COUNT(1) FROM mailboxes WHERE id=? AND user_id=? AND status='active'`, mailboxID, userID).Scan(&count)
if err != nil {
return false, err
}
return count > 0, nil
}
func (a *App) userOwnsMailboxAddress(ctx context.Context, userID, address string) (bool, error) {
var count int
err := a.db.QueryRowContext(ctx, `SELECT COUNT(1) FROM mailboxes WHERE user_id=? AND address=? AND status='active'`, userID, normalizeEmail(address)).Scan(&count)
if err != nil {
return false, err
}
return count > 0, nil
}
+1
View File
@@ -1007,6 +1007,7 @@ func (a *App) sendMailWithSource(ctx context.Context, user *User, mb *Mailbox, r
if inboxMsgID, err := a.insertMessage(ctx, copyMsg, req.Attachments); err == nil {
_ = a.writeStoredMessageToMaildir(ctx, inboxMsgID, copyMsg, req.Attachments)
a.applyInboundControls(ctx, inboxMsgID, rcptMailbox.ID, copyMsg.From, copyMsg.Subject)
a.processInboundForwarding(ctx, inboxMsgID, rcptMailbox.ID, mimeBytes)
}
}
+1
View File
@@ -371,6 +371,7 @@ func (a *App) syncMaildirFile(ctx context.Context, mb maildirMailbox, folder mai
id, err := a.insertMessage(ctx, msg, attachments)
if err == nil && strings.EqualFold(folder.Name, "Inbox") {
a.applyInboundControls(ctx, id, mb.ID, msg.From, msg.Subject)
a.processInboundForwarding(ctx, id, mb.ID, raw)
}
return err == nil, err
}
+5
View File
@@ -44,6 +44,11 @@ func (a *App) Router() http.Handler {
r.With(a.requireAuth).Delete("/me/api-tokens/{id}", a.handleDeleteAPIToken)
r.With(a.requireAuth, a.requirePermission(PermissionMailboxApply)).Get("/me/mailbox-apply-options", a.handleMailboxApplyOptions)
r.With(a.requireAuth, a.requirePermission(PermissionMailboxApply)).Post("/me/mailboxes/apply", a.handleApplyMailbox)
r.With(a.requireAuth, a.requirePermission(PermissionMailAccess)).Get("/me/forwarding", a.handleForwardingSettings)
r.With(a.requireAuth, a.requirePermission(PermissionMailAccess)).Post("/me/forwarding/verified-emails", a.handleAddForwardingVerifiedEmail)
r.With(a.requireAuth, a.requirePermission(PermissionMailAccess)).Delete("/me/forwarding/verified-emails/{id}", a.handleDeleteForwardingVerifiedEmail)
r.With(a.requireAuth, a.requirePermission(PermissionMailAccess)).Post("/me/forwarding/account", a.handleUpdateAccountForwarding)
r.With(a.requireAuth, a.requirePermission(PermissionMailAccess)).Post("/me/mailboxes/{id}/forwarding", a.handleUpdateMailboxForwarding)
r.With(a.requireAuth).Post("/me/2fa/setup", a.handleTwoFactorSetup)
r.With(a.requireAuth).Post("/me/2fa/enable", a.handleTwoFactorEnable)
r.With(a.requireAuth).Post("/me/2fa/disable", a.handleTwoFactorDisable)
+1
View File
@@ -28,6 +28,7 @@ const (
sendSourceWebmail = "webmail"
sendSourceSubmission = "submission"
sendSourceOpenAPI = "open_api"
sendSourceForwarding = "forwarding"
sendQueueStaleAfter = 15 * time.Minute
sendQueueConcurrency = 4