2026-06-14 01:07:48 +08:00
package app
import (
"context"
"database/sql"
"errors"
"net/http"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
type contextKey string
const userContextKey contextKey = "user"
2026-07-10 10:47:58 +08:00
const apiTokenScopesContextKey contextKey = "api_token_scopes"
2026-06-14 01:07:48 +08:00
func ( a * App ) Router () http . Handler {
r := chi . NewRouter ()
r . Use ( middleware . RequestID )
r . Use ( middleware . RealIP )
r . Use ( middleware . Logger )
r . Use ( middleware . Recoverer )
r . Use ( a . corsMiddleware )
2026-06-23 11:25:38 +08:00
r . Post ( "/auth-policy" , a . handleAuthPolicy )
2026-06-14 01:07:48 +08:00
r . Get ( "/healthz" , func ( w http . ResponseWriter , r * http . Request ) {
respondJSON ( w , http . StatusOK , map [ string ] any { "ok" : true , "time" : a . now (). UTC ()})
})
r . Route ( "/api" , func ( r chi . Router ) {
2026-06-15 00:37:43 +08:00
r . Get ( "/public/settings" , a . handlePublicSettings )
2026-08-02 09:08:20 +08:00
r . Get ( "/verify-email" , a . handleVerifyForwardingEmail )
2026-06-15 23:57:56 +08:00
r . Post ( "/auth/register" , a . handleRegister )
2026-06-14 01:07:48 +08:00
r . Post ( "/auth/login" , a . handleLogin )
r . Post ( "/auth/logout" , a . handleLogout )
r . With ( a . requireAuth ). Get ( "/me" , a . handleMe )
r . With ( a . requireAuth ). Post ( "/me/profile" , a . handleUpdateProfile )
r . With ( a . requireAuth ). Post ( "/me/password" , a . handleChangePassword )
2026-06-29 15:31:20 +08:00
r . With ( a . requireAuth ). Get ( "/me/api-tokens" , a . handleListAPITokens )
r . With ( a . requireAuth ). Post ( "/me/api-tokens" , a . handleCreateAPIToken )
r . With ( a . requireAuth ). Post ( "/me/api-tokens/{id}" , a . handleUpdateAPIToken )
r . With ( a . requireAuth ). Delete ( "/me/api-tokens/{id}" , a . handleDeleteAPIToken )
2026-06-22 15:54:15 +08:00
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 )
2026-08-02 07:37:07 +08:00
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 )
2026-08-02 09:08:20 +08:00
r . With ( a . requireAuth , a . requirePermission ( PermissionMailAccess )). Post ( "/me/forwarding/verified-emails/{id}/resend" , a . handleResendForwardingVerifiedEmail )
2026-08-02 07:37:07 +08:00
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 )
2026-06-15 00:37:43 +08:00
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 )
2026-06-22 15:54:15 +08:00
r . With ( a . requireAuth , a . requirePermission ( PermissionMailContacts )). Get ( "/me/contacts" , a . handleListContacts )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailContacts )). Post ( "/me/contacts" , a . handleCreateContact )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailContacts )). Delete ( "/me/contacts/{id}" , a . handleDeleteContact )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailSignatures )). Get ( "/me/signatures" , a . handleListSignatures )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailSignatures )). Post ( "/me/signatures" , a . handleCreateSignature )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailSignatures )). Post ( "/me/signatures/{id}" , a . handleUpdateSignature )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailSignatures )). Post ( "/me/signatures/{id}/default" , a . handleSetDefaultSignature )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailSignatures )). Delete ( "/me/signatures/{id}" , a . handleDeleteSignature )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailSignatures )). Get ( "/me/signatures/default" , a . handleDefaultSignature )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailRules )). Get ( "/me/rules" , a . handleListRules )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailRules )). Post ( "/me/rules" , a . handleCreateRule )
2026-08-03 16:16:20 +08:00
r . With ( a . requireAuth , a . requirePermission ( PermissionMailRules )). Post ( "/me/rules/{id}" , a . handleUpdateRule )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailRules )). Post ( "/me/rules/{id}/move" , a . handleMoveRule )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailRules )). Post ( "/me/rules/{id}/apply" , a . handleApplyRule )
2026-06-22 15:54:15 +08:00
r . With ( a . requireAuth , a . requirePermission ( PermissionMailRules )). Delete ( "/me/rules/{id}" , a . handleDeleteRule )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailBlocked )). Get ( "/me/blocked-senders" , a . handleListBlockedSenders )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailBlocked )). Post ( "/me/blocked-senders" , a . handleCreateBlockedSender )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailBlocked )). Delete ( "/me/blocked-senders/{id}" , a . handleDeleteBlockedSender )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailStats )). Get ( "/me/stats" , a . handleMailStats )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailOrganize )). Post ( "/me/cleanup" , a . handleMailCleanup )
2026-06-25 22:44:32 +08:00
r . With ( a . requireAuth , a . requirePermission ( PermissionMailAccess ), a . requireExternalIMAPEnabled ). Get ( "/me/external-imap-accounts" , a . handleListExternalIMAPAccounts )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailAccess ), a . requireExternalIMAPEnabled ). Post ( "/me/external-imap-accounts" , a . handleCreateExternalIMAPAccount )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailAccess ), a . requireExternalIMAPEnabled ). Post ( "/me/external-imap-accounts/{id}" , a . handleUpdateExternalIMAPAccount )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailAccess ), a . requireExternalIMAPEnabled ). Delete ( "/me/external-imap-accounts/{id}" , a . handleDeleteExternalIMAPAccount )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailAccess ), a . requireExternalIMAPEnabled ). Post ( "/me/external-imap-accounts/{id}/test" , a . handleTestExternalIMAPAccount )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailAccess ), a . requireExternalIMAPEnabled ). Get ( "/me/external-imap-accounts/{id}/runs" , a . handleExternalIMAPSyncRuns )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailAccess ), a . requireExternalIMAPEnabled ). Post ( "/me/external-imap-accounts/{id}/sync" , a . handleSyncExternalIMAPAccount )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailAccess ), a . requireExternalIMAPEnabled ). Post ( "/me/external-imap-accounts/{id}/sync-folder" , a . handleSyncExternalIMAPFolder )
r . With ( a . requireAuth , a . requirePermission ( PermissionMailAccess ), a . requireExternalIMAPEnabled ). Post ( "/me/external-imap-oauth/{provider}/start" , a . handleStartExternalIMAPOAuth )
r . With ( a . requireExternalIMAPEnabled ). Get ( "/external-imap-oauth/{provider}/callback" , a . handleExternalIMAPOAuthCallback )
2026-06-14 01:07:48 +08:00
r . With ( a . requireAuth ). Get ( "/events" , a . handleEvents )
2026-07-10 10:47:58 +08:00
r . Post ( "/open/v1/delivery-events" , a . handleOpenAPIDeliveryWebhook )
r . Route ( "/open" , func ( r chi . Router ) { a . registerOpenAPIRoutes ( r ) })
r . Route ( "/open/v1" , func ( r chi . Router ) { a . registerOpenAPIRoutes ( r ) })
2026-06-29 15:31:20 +08:00
2026-06-14 01:07:48 +08:00
r . Group ( func ( r chi . Router ) {
r . Use ( a . requireAuth )
2026-06-22 15:54:15 +08:00
r . With ( a . requirePermission ( PermissionMailAccess )). Get ( "/mail/mailboxes" , a . handleMyMailboxes )
r . With ( a . requirePermission ( PermissionMailRead )). Get ( "/mail/folders" , a . handleMailFolders )
2026-06-25 14:11:38 +08:00
r . With ( a . requirePermission ( PermissionMailOrganize )). Post ( "/mail/folders" , a . handleCreateMailFolder )
2026-06-25 15:42:36 +08:00
r . With ( a . requirePermission ( PermissionMailOrganize )). Post ( "/mail/folders/reorder" , a . handleReorderMailFolders )
2026-06-25 15:56:57 +08:00
r . With ( a . requirePermission ( PermissionMailOrganize )). Delete ( "/mail/folders/{id}" , a . handleDeleteMailFolder )
2026-06-22 15:54:15 +08:00
r . With ( a . requireAnyPermission ( PermissionMailRead , PermissionMailLabels )). Get ( "/mail/labels" , a . handleMailLabels )
r . With ( a . requirePermission ( PermissionMailLabels )). Post ( "/mail/labels" , a . handleCreateMailLabel )
2026-06-22 13:54:32 +08:00
r . With ( a . requirePermission ( PermissionMailLabels )). Delete ( "/mail/labels/{id}" , a . handleDeleteMailLabel )
2026-06-22 15:54:15 +08:00
r . With ( a . requirePermission ( PermissionMailRead )). Get ( "/mail/messages" , a . handleMailMessages )
r . With ( a . requirePermission ( PermissionMailRead )). Get ( "/mail/starred" , a . handleStarredMessages )
2026-08-03 16:16:20 +08:00
r . With ( a . requirePermission ( PermissionMailRead )). Get ( "/mail/export" , a . handleExportMail )
r . With ( a . requirePermission ( PermissionMailOrganize )). Post ( "/mail/import" , a . handleImportMail )
2026-06-22 15:54:15 +08:00
r . With ( a . requirePermission ( PermissionMailRead )). Get ( "/mail/messages/{id}" , a . handleMailMessage )
2026-06-27 13:22:45 +08:00
r . With ( a . requirePermission ( PermissionMailRead )). Post ( "/mail/messages/{id}/translate" , a . handleTranslateMailMessage )
2026-06-25 22:44:32 +08:00
r . With ( a . requirePermission ( PermissionMailRead ), a . requireExternalIMAPEnabled ). Get ( "/mail/external-accounts" , a . handleMailExternalAccounts )
r . With ( a . requirePermission ( PermissionMailRead ), a . requireExternalIMAPEnabled ). Get ( "/mail/external-accounts/{id}/folders" , a . handleExternalIMAPFolders )
r . With ( a . requirePermission ( PermissionMailRead ), a . requireExternalIMAPEnabled ). Get ( "/mail/external-accounts/{id}/messages" , a . handleExternalIMAPMessages )
r . With ( a . requirePermission ( PermissionMailRead ), a . requireExternalIMAPEnabled ). Get ( "/mail/external-accounts/{id}/messages/{remoteId}" , a . handleExternalIMAPMessage )
2026-06-27 13:30:31 +08:00
r . With ( a . requirePermission ( PermissionMailRead ), a . requireExternalIMAPEnabled ). Post ( "/mail/external-accounts/{id}/messages/{remoteId}/translate" , a . handleTranslateExternalIMAPMessage )
2026-06-25 22:44:32 +08:00
r . With ( a . requirePermission ( PermissionMailAttachments ), a . requireExternalIMAPEnabled ). Get ( "/mail/external-accounts/{id}/attachments/{remoteId}/{partId}" , a . handleExternalIMAPAttachment )
r . With ( a . requirePermission ( PermissionMailOrganize ), a . requireExternalIMAPEnabled ). Post ( "/mail/external-accounts/{id}/messages/{remoteId}/mark-read" , a . handleExternalIMAPMarkRead )
2026-06-22 15:54:15 +08:00
r . With ( a . requirePermission ( PermissionMailSend )). Post ( "/mail/send" , a . handleMailSend )
2026-06-24 16:39:40 +08:00
r . With ( a . requirePermission ( PermissionMailRead )). Get ( "/mail/send-queue" , a . handleSendQueue )
r . With ( a . requirePermission ( PermissionMailRead )). Get ( "/mail/send-queue/{id}/audit" , a . handleSendQueueAudit )
r . With ( a . requirePermission ( PermissionMailSend )). Post ( "/mail/send-queue/{id}/retry" , a . handleRetrySendQueue )
r . With ( a . requirePermission ( PermissionMailSend )). Delete ( "/mail/send-queue/{id}" , a . handleCancelSendQueue )
2026-06-22 15:54:15 +08:00
r . With ( a . requirePermission ( PermissionMailSchedule )). Get ( "/mail/scheduled-sends" , a . handleScheduledSends )
r . With ( a . requirePermission ( PermissionMailSchedule ), a . requirePermission ( PermissionMailSend )). Post ( "/mail/schedule-send" , a . handleScheduleSend )
r . With ( a . requirePermission ( PermissionMailSchedule )). Delete ( "/mail/schedule-send/{id}" , a . handleCancelScheduledSend )
r . With ( a . requirePermission ( PermissionMailDrafts )). Post ( "/mail/drafts" , a . handleSaveDraft )
r . With ( a . requirePermission ( PermissionMailDrafts )). Post ( "/mail/drafts/{id}" , a . handleSaveDraft )
r . With ( a . requirePermission ( PermissionMailDrafts )). Delete ( "/mail/drafts/{id}" , a . handleDeleteDraft )
r . With ( a . requirePermission ( PermissionMailOrganize )). Post ( "/mail/messages/{id}/mark-read" , a . handleMarkRead )
r . With ( a . requirePermission ( PermissionMailOrganize )). Post ( "/mail/messages/{id}/star" , a . handleStar )
r . With ( a . requirePermission ( PermissionMailLabels )). Post ( "/mail/messages/{id}/labels" , a . handleAddMessageLabel )
r . With ( a . requirePermission ( PermissionMailLabels )). Delete ( "/mail/messages/{id}/labels/{labelID}" , a . handleRemoveMessageLabel )
2026-08-05 02:55:36 +08:00
r . With ( a . requirePermission ( PermissionMailOrganize )). Post ( "/mail/messages/bulk-move" , a . handleBulkMove )
2026-06-22 15:54:15 +08:00
r . With ( a . requirePermission ( PermissionMailOrganize )). Post ( "/mail/messages/{id}/move" , a . handleMove )
r . With ( a . requirePermission ( PermissionMailOrganize )). Delete ( "/mail/messages/{id}" , a . handleDeleteMessage )
r . With ( a . requirePermission ( PermissionMailAttachments )). Get ( "/mail/attachments/{id}" , a . handleAttachment )
2026-06-14 01:07:48 +08:00
})
r . Group ( func ( r chi . Router ) {
r . Use ( a . requireAuth )
2026-06-22 14:31:10 +08:00
r . Use ( a . requireAdminAccess )
2026-08-03 16:16:20 +08:00
r . Get ( "/admin/system/version" , a . handleSystemVersion )
r . Post ( "/admin/system/update" , a . handleSystemUpdate )
2026-06-22 14:31:10 +08:00
r . With ( a . requirePermission ( PermissionAdminOverview )). Get ( "/admin/overview" , a . handleAdminOverview )
r . With ( a . requireAnyPermission ( PermissionUsersView , PermissionMailboxesView )). Get ( "/admin/users" , a . handleListUsers )
r . With ( a . requirePermission ( PermissionUsersCreate )). Post ( "/admin/users" , a . handleCreateUser )
r . With ( a . requirePermission ( PermissionUsersUpdate )). Post ( "/admin/users/{id}" , a . handleUpdateUser )
r . With ( a . requirePermission ( PermissionUsersResetPassword )). Post ( "/admin/users/{id}/password" , a . handleResetUserPassword )
r . With ( a . requirePermission ( PermissionUsersDelete )). Delete ( "/admin/users/{id}" , a . handleDeleteUser )
2026-06-23 11:25:38 +08:00
r . With ( a . requireAnyPermission ( PermissionGroupsView , PermissionUsersView )). Get ( "/admin/permission-limits/defaults" , a . handleDefaultPermissionLimits )
2026-06-22 14:31:10 +08:00
r . With ( a . requireAnyPermission ( PermissionGroupsView , PermissionUsersView )). Get ( "/admin/permissions" , a . handlePermissionCatalog )
r . With ( a . requireAnyPermission ( PermissionGroupsView , PermissionUsersView )). Get ( "/admin/permission-groups" , a . handleListPermissionGroups )
r . With ( a . requirePermission ( PermissionGroupsCreate )). Post ( "/admin/permission-groups" , a . handleCreatePermissionGroup )
r . With ( a . requirePermission ( PermissionGroupsUpdate )). Post ( "/admin/permission-groups/{id}" , a . handleUpdatePermissionGroup )
r . With ( a . requirePermission ( PermissionGroupsDelete )). Delete ( "/admin/permission-groups/{id}" , a . handleDeletePermissionGroup )
r . With ( a . requireAnyPermission ( PermissionDomainsView , PermissionDNSView , PermissionMailboxesView , PermissionAliasesView , PermissionSettingsView , PermissionTemplatesView )). Get ( "/admin/domains" , a . handleListDomains )
r . With ( a . requirePermission ( PermissionDomainsCreate )). Post ( "/admin/domains" , a . handleCreateDomain )
r . With ( a . requirePermission ( PermissionDomainsUpdate )). Post ( "/admin/domains/{id}" , a . handleUpdateDomain )
r . With ( a . requirePermission ( PermissionDomainsDelete )). Delete ( "/admin/domains/{id}" , a . handleDeleteDomain )
r . With ( a . requireAnyPermission ( PermissionMailboxesView , PermissionMessagesView )). Get ( "/admin/mailboxes" , a . handleListMailboxes )
r . With ( a . requirePermission ( PermissionMailboxesCreate )). Post ( "/admin/mailboxes" , a . handleCreateMailbox )
r . With ( a . requirePermission ( PermissionMailboxesUpdate )). Post ( "/admin/mailboxes/{id}" , a . handleUpdateMailbox )
r . With ( a . requirePermission ( PermissionMailboxesDelete )). Delete ( "/admin/mailboxes/{id}" , a . handleDeleteMailbox )
r . With ( a . requirePermission ( PermissionAliasesView )). Get ( "/admin/aliases" , a . handleListAliases )
r . With ( a . requirePermission ( PermissionAliasesCreate )). Post ( "/admin/aliases" , a . handleCreateAlias )
r . With ( a . requirePermission ( PermissionAliasesUpdate )). Post ( "/admin/aliases/{id}" , a . handleUpdateAlias )
r . With ( a . requirePermission ( PermissionAliasesDelete )). Delete ( "/admin/aliases/{id}" , a . handleDeleteAlias )
r . With ( a . requirePermission ( PermissionMessagesView )). Get ( "/admin/messages" , a . handleAdminMessages )
2026-06-24 17:16:01 +08:00
r . With ( a . requirePermission ( PermissionMessagesView )). Get ( "/admin/send-audit" , a . handleAdminSendAudit )
2026-06-22 14:31:10 +08:00
r . With ( a . requirePermission ( PermissionMessagesRead )). Get ( "/admin/messages/{id}" , a . handleAdminMessage )
r . With ( a . requirePermission ( PermissionMessagesAttachment )). Get ( "/admin/attachments/{id}" , a . handleAdminAttachment )
r . With ( a . requirePermission ( PermissionSettingsView )). Get ( "/admin/settings" , a . handleGetSystemSettings )
2026-06-24 16:08:48 +08:00
r . With ( a . requirePermission ( PermissionSettingsView )). Get ( "/admin/maildir-sync/health" , a . handleMaildirSyncHealth )
2026-06-22 14:31:10 +08:00
r . With ( a . requirePermission ( PermissionSettingsUpdate )). Post ( "/admin/settings" , a . handleUpdateSystemSettings )
r . With ( a . requirePermission ( PermissionSettingsTestSMTP )). Post ( "/admin/settings/test-smtp" , a . handleTestSMTP )
2026-08-06 17:13:08 +08:00
r . With ( a . requirePermission ( PermissionSettingsUpdate )). Post ( "/admin/settings/telegram/pair" , a . handleCreateTelegramPairing )
2026-08-06 02:17:11 +08:00
r . With ( a . requirePermission ( PermissionSettingsUpdate )). Post ( "/admin/settings/telegram/discover" , a . handleDiscoverTelegramChat )
r . With ( a . requirePermission ( PermissionSettingsUpdate )). Post ( "/admin/settings/telegram/test" , a . handleTestTelegram )
2026-06-22 14:31:10 +08:00
r . With ( a . requirePermission ( PermissionTemplatesView )). Get ( "/admin/mail-templates" , a . handleListMailTemplates )
r . With ( a . requirePermission ( PermissionTemplatesUpdate )). Post ( "/admin/mail-templates/{key}" , a . handleUpdateMailTemplate )
r . With ( a . requirePermission ( PermissionTemplatesReset )). Post ( "/admin/mail-templates/{key}/reset" , a . handleResetMailTemplate )
r . With ( a . requirePermission ( PermissionDNSView )). Get ( "/admin/domains/{id}/dns-records" , a . handleDNSRecords )
r . With ( a . requirePermission ( PermissionDNSCheck )). Post ( "/admin/domains/{id}/check-dns" , a . handleDNSCheck )
2026-06-14 01:07:48 +08:00
})
})
return r
}
2026-07-10 10:47:58 +08:00
func ( a * App ) registerOpenAPIRoutes ( r chi . Router ) {
r . Use ( a . requireAPIToken )
r . With ( a . requireAPITokenScope ( "domains:read" ), a . requireAdminAccess , a . requireAnyPermission ( PermissionDomainsView , PermissionDNSView , PermissionMailboxesView , PermissionAliasesView , PermissionSettingsView , PermissionTemplatesView )). Get ( "/domains" , a . handleOpenAPIListDomains )
r . With ( a . requireAPITokenScope ( "domains:write" ), a . requireAdminAccess , a . requirePermission ( PermissionDomainsCreate )). Post ( "/domains" , a . handleOpenAPICreateDomain )
r . With ( a . requireAPITokenScope ( "domains:read" ), a . requireAdminAccess , a . requireAnyPermission ( PermissionDomainsView , PermissionDNSView , PermissionMailboxesView , PermissionAliasesView , PermissionSettingsView , PermissionTemplatesView )). Get ( "/domains/{id}" , a . handleOpenAPIGetDomain )
r . With ( a . requireAPITokenScope ( "domains:write" ), a . requireAdminAccess , a . requirePermission ( PermissionDomainsUpdate )). Post ( "/domains/{id}" , a . handleOpenAPIUpdateDomain )
r . With ( a . requireAPITokenScope ( "domains:write" ), a . requireAdminAccess , a . requirePermission ( PermissionDomainsDelete )). Delete ( "/domains/{id}" , a . handleOpenAPIDeleteDomain )
r . With ( a . requireAPITokenScope ( "dns:read" ), a . requireAdminAccess , a . requirePermission ( PermissionDNSView )). Get ( "/domains/{id}/dns-records" , a . handleDNSRecords )
r . With ( a . requireAPITokenScope ( "dns:check" ), a . requireAdminAccess , a . requirePermission ( PermissionDNSCheck )). Post ( "/domains/{id}/dns-check" , a . handleDNSCheck )
r . With ( a . requireAPITokenScope ( "mailboxes:read" ), a . requireAdminAccess , a . requireAnyPermission ( PermissionMailboxesView , PermissionMessagesView )). Get ( "/mailboxes" , a . handleOpenAPIListMailboxes )
r . With ( a . requireAPITokenScope ( "mailboxes:write" ), a . requireAdminAccess , a . requirePermission ( PermissionMailboxesCreate )). Post ( "/mailboxes" , a . handleOpenAPICreateMailbox )
r . With ( a . requireAPITokenScope ( "mailboxes:read" ), a . requireAdminAccess , a . requireAnyPermission ( PermissionMailboxesView , PermissionMessagesView )). Get ( "/mailboxes/{id}" , a . handleOpenAPIGetMailbox )
r . With ( a . requireAPITokenScope ( "mailboxes:write" ), a . requireAdminAccess , a . requirePermission ( PermissionMailboxesUpdate )). Post ( "/mailboxes/{id}" , a . handleOpenAPIUpdateMailbox )
r . With ( a . requireAPITokenScope ( "mailboxes:write" ), a . requireAdminAccess , a . requirePermission ( PermissionUsersResetPassword )). Post ( "/mailboxes/{id}/password" , a . handleOpenAPIResetMailboxPassword )
r . With ( a . requireAPITokenScope ( "mailboxes:write" ), a . requireAdminAccess , a . requirePermission ( PermissionMailboxesDelete )). Delete ( "/mailboxes/{id}" , a . handleOpenAPIDeleteMailbox )
r . With ( a . requireAPITokenScope ( "messages:send" ), a . requirePermission ( PermissionMailSend )). Post ( "/send" , a . handleOpenAPISendMail )
r . With ( a . requireAPITokenScope ( "messages:read" ), a . requirePermission ( PermissionMailRead )). Get ( "/send" , a . handleOpenAPIListSends )
r . With ( a . requireAPITokenScope ( "messages:read" ), a . requirePermission ( PermissionMailRead )). Get ( "/send/{id}" , a . handleOpenAPISendStatus )
r . With ( a . requireAPITokenScope ( "messages:read" ), a . requirePermission ( PermissionMailRead )). Get ( "/send/{id}/events" , a . handleOpenAPISendEvents )
r . With ( a . requireAPITokenScope ( "messages:manage" ), a . requirePermission ( PermissionMailSend )). Post ( "/send/{id}/retry" , a . handleOpenAPIRetrySend )
r . With ( a . requireAPITokenScope ( "messages:manage" ), a . requirePermission ( PermissionMailSend )). Post ( "/send/{id}/cancel" , a . handleOpenAPICancelSend )
r . With ( a . requireAPITokenScope ( "messages:read" ), a . requirePermission ( PermissionMailRead )). Get ( "/mailboxes/{id}/messages" , a . handleOpenAPIMailboxMessages )
r . With ( a . requireAPITokenScope ( "messages:read" ), a . requirePermission ( PermissionMailRead )). Get ( "/messages/{id}" , a . handleOpenAPIMessage )
r . With ( a . requireAPITokenScope ( "messages:read" ), a . requirePermission ( PermissionMailAttachments )). Get ( "/attachments/{id}" , a . handleAttachment )
r . With ( a . requireAPITokenScope ( "aliases:read" ), a . requireAdminAccess , a . requirePermission ( PermissionAliasesView )). Get ( "/aliases" , a . handleOpenAPIListAliases )
r . With ( a . requireAPITokenScope ( "aliases:write" ), a . requireAdminAccess , a . requirePermission ( PermissionAliasesCreate )). Post ( "/aliases" , a . handleCreateAlias )
r . With ( a . requireAPITokenScope ( "aliases:read" ), a . requireAdminAccess , a . requirePermission ( PermissionAliasesView )). Get ( "/aliases/{id}" , a . handleOpenAPIGetAlias )
r . With ( a . requireAPITokenScope ( "aliases:write" ), a . requireAdminAccess , a . requirePermission ( PermissionAliasesUpdate )). Post ( "/aliases/{id}" , a . handleUpdateAlias )
r . With ( a . requireAPITokenScope ( "aliases:write" ), a . requireAdminAccess , a . requirePermission ( PermissionAliasesDelete )). Delete ( "/aliases/{id}" , a . handleDeleteAlias )
}
2026-06-14 01:07:48 +08:00
func ( a * App ) corsMiddleware ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
origin := r . Header . Get ( "Origin" )
2026-08-03 19:51:27 +08:00
if origin != "" && ( strings . HasPrefix ( origin , "http://localhost:" ) || strings . HasPrefix ( origin , "http://127.0.0.1:" ) || origin == a . config (). PublicBaseURL ) {
2026-06-14 01:07:48 +08:00
w . Header (). Set ( "Access-Control-Allow-Origin" , origin )
w . Header (). Set ( "Vary" , "Origin" )
w . Header (). Set ( "Access-Control-Allow-Credentials" , "true" )
2026-07-10 10:47:58 +08:00
w . Header (). Set ( "Access-Control-Allow-Headers" , "Content-Type, Authorization, Idempotency-Key" )
2026-06-14 01:07:48 +08:00
w . Header (). Set ( "Access-Control-Allow-Methods" , "GET,POST,DELETE,OPTIONS" )
}
if r . Method == http . MethodOptions {
w . WriteHeader ( http . StatusNoContent )
return
}
next . ServeHTTP ( w , r )
})
}
func ( a * App ) requireAuth ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
user , err := a . authenticateRequest ( r )
if err != nil {
respondError ( w , http . StatusUnauthorized , "authentication required" )
return
}
next . ServeHTTP ( w , r . WithContext ( context . WithValue ( r . Context (), userContextKey , user )))
})
}
2026-06-29 15:31:20 +08:00
func ( a * App ) requireAPIToken ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
2026-07-10 10:47:58 +08:00
user , scopes , err := a . authenticateAPIToken ( r )
2026-06-29 15:31:20 +08:00
if err != nil {
respondError ( w , http . StatusUnauthorized , "api token required" )
return
}
2026-07-10 10:47:58 +08:00
ctx := context . WithValue ( r . Context (), userContextKey , user )
ctx = context . WithValue ( ctx , apiTokenScopesContextKey , scopes )
next . ServeHTTP ( w , r . WithContext ( ctx ))
2026-06-29 15:31:20 +08:00
})
}
2026-07-10 10:47:58 +08:00
func ( a * App ) requireAPITokenScope ( scope string ) func ( http . Handler ) http . Handler {
return func ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
scopes , _ := r . Context (). Value ( apiTokenScopesContextKey ).( map [ string ] bool )
if ! scopes [ "*" ] && ! scopes [ scope ] {
respondError ( w , http . StatusForbidden , "api token scope required: " + scope )
return
}
next . ServeHTTP ( w , r )
})
}
}
2026-06-14 01:07:48 +08:00
func currentUser ( r * http . Request ) * User {
user , _ := r . Context (). Value ( userContextKey ).( * User )
return user
}
func ( a * App ) authenticateRequest ( r * http . Request ) ( * User , error ) {
2026-08-03 19:51:27 +08:00
cookie , err := r . Cookie ( a . config (). CookieName )
2026-06-14 01:07:48 +08:00
if err != nil || cookie . Value == "" {
return nil , errors . New ( "no session" )
}
2026-08-02 16:56:04 +08:00
row := a . db . QueryRowContext ( r . Context (), `SELECT u.id,u.login_name,u.email,u.display_name,u.role,u.disabled,u.two_factor_enabled,u.mailbox_limit_override,u.created_at
2026-06-14 01:07:48 +08:00
FROM sessions s JOIN users u ON u.id=s.user_id
WHERE s.token_hash=? AND s.expires_at > ?` , hashToken ( cookie . Value ), a . now (). UTC (). Format ( time . RFC3339Nano ))
var u User
2026-06-15 00:37:43 +08:00
var disabled , twoFactorEnabled int
2026-08-02 15:07:16 +08:00
var mailboxLimitOverride sql . NullInt64
2026-06-14 01:07:48 +08:00
var created string
2026-08-02 16:56:04 +08:00
if err := row . Scan ( & u . ID , & u . LoginName , & u . Email , & u . DisplayName , & u . Role , & disabled , & twoFactorEnabled , & mailboxLimitOverride , & created ); err != nil {
2026-06-14 01:07:48 +08:00
return nil , err
}
u . Disabled = intBool ( disabled )
2026-06-15 00:37:43 +08:00
u . TwoFactorEnabled = intBool ( twoFactorEnabled )
2026-08-02 15:07:16 +08:00
u . MailboxLimitOverride = intPtrFromNull ( mailboxLimitOverride )
2026-06-14 01:07:48 +08:00
u . CreatedAt = parseTime ( created )
if u . Disabled {
return nil , errors . New ( "disabled" )
}
2026-06-22 14:31:10 +08:00
if err := a . attachUserAuthorization ( r . Context (), & u ); err != nil {
return nil , err
}
2026-06-14 01:07:48 +08:00
return & u , nil
}
2026-07-10 10:47:58 +08:00
func ( a * App ) authenticateAPIToken ( r * http . Request ) ( * User , map [ string ] bool , error ) {
2026-06-29 15:31:20 +08:00
token := bearerToken ( r )
if token == "" {
2026-07-10 10:47:58 +08:00
return nil , nil , errors . New ( "no api token" )
2026-06-29 15:31:20 +08:00
}
now := a . now (). UTC (). Format ( time . RFC3339Nano )
2026-08-02 16:56:04 +08:00
row := a . db . QueryRowContext ( r . Context (), `SELECT at.id,at.scopes_json,u.id,u.login_name,u.email,u.display_name,u.role,u.disabled,u.two_factor_enabled,u.mailbox_limit_override,u.created_at
2026-06-29 15:31:20 +08:00
FROM api_tokens at JOIN users u ON u.id=at.user_id
2026-06-29 16:09:40 +08:00
WHERE at.token_hash=? AND at.disabled=0 AND at.expires_at > ?` , hashToken ( token ), now )
2026-07-10 10:47:58 +08:00
var tokenID , scopesJSON string
2026-06-29 15:31:20 +08:00
var u User
var disabled , twoFactorEnabled int
2026-08-02 15:07:16 +08:00
var mailboxLimitOverride sql . NullInt64
2026-06-29 15:31:20 +08:00
var created string
2026-08-02 16:56:04 +08:00
if err := row . Scan ( & tokenID , & scopesJSON , & u . ID , & u . LoginName , & u . Email , & u . DisplayName , & u . Role , & disabled , & twoFactorEnabled , & mailboxLimitOverride , & created ); err != nil {
2026-07-10 10:47:58 +08:00
return nil , nil , err
2026-06-29 15:31:20 +08:00
}
u . Disabled = intBool ( disabled )
u . TwoFactorEnabled = intBool ( twoFactorEnabled )
2026-08-02 15:07:16 +08:00
u . MailboxLimitOverride = intPtrFromNull ( mailboxLimitOverride )
2026-06-29 15:31:20 +08:00
u . CreatedAt = parseTime ( created )
if u . Disabled {
2026-07-10 10:47:58 +08:00
return nil , nil , errors . New ( "disabled" )
2026-06-29 15:31:20 +08:00
}
if err := a . attachUserAuthorization ( r . Context (), & u ); err != nil {
2026-07-10 10:47:58 +08:00
return nil , nil , err
2026-06-29 15:31:20 +08:00
}
_ , _ = a . db . ExecContext ( r . Context (), `UPDATE api_tokens SET last_used_at=? WHERE id=?` , now , tokenID )
2026-07-10 10:47:58 +08:00
scopes := map [ string ] bool {}
for _ , scope := range jsonDecodeSlice ( scopesJSON ) {
scopes [ scope ] = true
}
return & u , scopes , nil
2026-06-29 15:31:20 +08:00
}
func bearerToken ( r * http . Request ) string {
fields := strings . Fields ( strings . TrimSpace ( r . Header . Get ( "Authorization" )))
if len ( fields ) != 2 || ! strings . EqualFold ( fields [ 0 ], "Bearer" ) {
return ""
}
return fields [ 1 ]
}
2026-06-14 01:07:48 +08:00
func ( a * App ) userByEmail ( ctx context . Context , email string ) ( * User , string , error ) {
2026-08-05 02:55:36 +08:00
email = normalizeEmail ( email )
2026-08-02 16:56:04 +08:00
row := a . db . QueryRowContext ( ctx , `SELECT id,login_name,email,display_name,role,password_hash,disabled,two_factor_enabled,mailbox_limit_override,created_at
2026-08-05 02:55:36 +08:00
FROM users WHERE email=? LIMIT 1` , email )
2026-06-14 01:07:48 +08:00
var u User
var passwordHash string
2026-06-15 00:37:43 +08:00
var disabled , twoFactorEnabled int
2026-08-02 15:07:16 +08:00
var mailboxLimitOverride sql . NullInt64
2026-06-14 01:07:48 +08:00
var created string
2026-08-02 16:56:04 +08:00
if err := row . Scan ( & u . ID , & u . LoginName , & u . Email , & u . DisplayName , & u . Role , & passwordHash , & disabled , & twoFactorEnabled , & mailboxLimitOverride , & created ); err != nil {
2026-06-14 01:07:48 +08:00
if errors . Is ( err , sql . ErrNoRows ) {
return nil , "" , errNotFound
}
return nil , "" , err
}
u . Disabled = intBool ( disabled )
2026-06-15 00:37:43 +08:00
u . TwoFactorEnabled = intBool ( twoFactorEnabled )
2026-08-02 15:07:16 +08:00
u . MailboxLimitOverride = intPtrFromNull ( mailboxLimitOverride )
2026-06-14 01:07:48 +08:00
u . CreatedAt = parseTime ( created )
2026-06-22 14:31:10 +08:00
if err := a . attachUserAuthorization ( ctx , & u ); err != nil {
return nil , "" , err
}
2026-06-14 01:07:48 +08:00
return & u , passwordHash , nil
}
func ( a * App ) userByID ( ctx context . Context , id string ) ( * User , error ) {
2026-08-02 16:56:04 +08:00
row := a . db . QueryRowContext ( ctx , `SELECT id,login_name,email,display_name,role,disabled,two_factor_enabled,mailbox_limit_override,created_at FROM users WHERE id=?` , id )
2026-06-14 01:07:48 +08:00
var u User
2026-06-15 00:37:43 +08:00
var disabled , twoFactorEnabled int
2026-08-02 15:07:16 +08:00
var mailboxLimitOverride sql . NullInt64
2026-06-14 01:07:48 +08:00
var created string
2026-08-02 16:56:04 +08:00
if err := row . Scan ( & u . ID , & u . LoginName , & u . Email , & u . DisplayName , & u . Role , & disabled , & twoFactorEnabled , & mailboxLimitOverride , & created ); err != nil {
2026-06-14 01:07:48 +08:00
if errors . Is ( err , sql . ErrNoRows ) {
return nil , errNotFound
}
return nil , err
}
u . Disabled = intBool ( disabled )
2026-06-15 00:37:43 +08:00
u . TwoFactorEnabled = intBool ( twoFactorEnabled )
2026-08-02 15:07:16 +08:00
u . MailboxLimitOverride = intPtrFromNull ( mailboxLimitOverride )
2026-06-14 01:07:48 +08:00
u . CreatedAt = parseTime ( created )
2026-06-22 14:31:10 +08:00
if err := a . attachUserAuthorization ( ctx , & u ); err != nil {
return nil , err
}
2026-06-14 01:07:48 +08:00
return & u , nil
}