mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-27 16:02:28 +00:00
293 lines
13 KiB
Go
293 lines
13 KiB
Go
package errx
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/warmbly/warmbly/internal/config"
|
|
)
|
|
|
|
type MailErrorLogType string
|
|
|
|
const (
|
|
MailErrorLogTypeExternal MailErrorLogType = "EXTERNAL"
|
|
MailErrorLogTypeInternal MailErrorLogType = "INTERNAL"
|
|
)
|
|
|
|
func (l MailErrorLogType) IsExternal() bool {
|
|
return l == MailErrorLogTypeExternal
|
|
}
|
|
|
|
type MailErrorType string
|
|
|
|
const (
|
|
MailErrorCritical MailErrorType = "CRITICAL"
|
|
MailErrorWarning MailErrorType = "WARNING"
|
|
MailErrorInformational MailErrorType = "INFORMATIONAL"
|
|
)
|
|
|
|
type MailErrorCode string
|
|
|
|
const (
|
|
MailErrorCodeFolderLimit MailErrorCode = "MAX_FOLDERS_REACHED"
|
|
// MailErrorCodeFolderConflict is two folders on one mailbox reporting
|
|
// the same UIDVALIDITY, the id a folder is stored under. Kept apart from
|
|
// the folder limit because the way out is renaming the folder, not
|
|
// deleting folders to get under a cap.
|
|
MailErrorCodeFolderConflict MailErrorCode = "FOLDER_ID_CONFLICT"
|
|
MailErrorCodeUpdateLimit MailErrorCode = "MAX_FOLDERS_REACHED"
|
|
MailErrorCodeGoogleAuth MailErrorCode = "GOOGLE_AUTHENTICATION_FAILED"
|
|
MailErrorCodeGooglePayment MailErrorCode = "GOOGLE_PAYMENT_REQUIRED"
|
|
MailErrorCodeGoogleForbidden MailErrorCode = "GOOGLE_FORBIDDEN"
|
|
|
|
MailErrorCodeServerUnreachable MailErrorCode = "SERVER_UNREACHABLE"
|
|
// MailErrorCodeNotFound is the provider saying the addressed resource is
|
|
// absent, kept apart from SERVER_UNREACHABLE so "this folder does not
|
|
// exist" is never inferred from a catch-all that also covers a 503.
|
|
MailErrorCodeNotFound MailErrorCode = "RESOURCE_NOT_FOUND"
|
|
MailErrorCodeUnsupported MailErrorCode = "UNSUPPORTED"
|
|
MailErrorCodeInvalidCredentials MailErrorCode = "INVALID_CREDENTIALS" // e.g. invalid username or password
|
|
MailErrorCodeAuthorizationFailed MailErrorCode = "AUTHORIZATION_FAILED" // e.g. imap disabled
|
|
MailErrorCodeAuthenticationFailed MailErrorCode = "AUTHENTICATION_FAILED" // e.g. invalid token
|
|
MailErrorCodeConnectionLost MailErrorCode = "CONNECTION_LOST"
|
|
MailErrorCodeImapUnknown MailErrorCode = "IMAP_UNKNOWN"
|
|
|
|
// Rate limiting and abuse detection
|
|
MailErrorCodeRateLimitExceeded MailErrorCode = "RATE_LIMIT_EXCEEDED"
|
|
// Sync fair use (internal/app/worker/wmail/governor.go). SYNC_FLOOD is
|
|
// an hourly volume no real mailbox produces; SYNC_FAIR_USE is repeated
|
|
// daily overage. Both deactivate the mailbox until someone reactivates it.
|
|
MailErrorCodeSyncFlood MailErrorCode = "SYNC_FLOOD"
|
|
MailErrorCodeSyncFairUse MailErrorCode = "SYNC_FAIR_USE"
|
|
MailErrorCodeSendingTooFast MailErrorCode = "SENDING_TOO_FAST"
|
|
MailErrorCodeRecipientRejected MailErrorCode = "RECIPIENT_REJECTED"
|
|
// MailErrorCodeDomainAuthRejected is the receiving side refusing the mail
|
|
// because the SENDING DOMAIN failed its authentication bar (Outlook's
|
|
// 5.7.515, Gmail's 5.7.26). Not a dead server and not a bad recipient:
|
|
// retrying from the same domain fails identically until DNS is fixed.
|
|
MailErrorCodeDomainAuthRejected MailErrorCode = "DOMAIN_AUTH_REJECTED"
|
|
MailErrorCodeQuotaExceeded MailErrorCode = "QUOTA_EXCEEDED"
|
|
MailErrorCodeAccountSuspended MailErrorCode = "ACCOUNT_SUSPENDED"
|
|
)
|
|
|
|
// CredentialMailErrorCodes are the credential-class errors a successful
|
|
// mailbox re-authorization or SMTP/IMAP credential update fixes.
|
|
var CredentialMailErrorCodes = []MailErrorCode{
|
|
MailErrorCodeGoogleAuth,
|
|
MailErrorCodeAuthenticationFailed,
|
|
MailErrorCodeAuthorizationFailed,
|
|
MailErrorCodeInvalidCredentials,
|
|
}
|
|
|
|
var MailErrorCodeGoogleUnknown = func(code int) MailErrorCode {
|
|
return MailErrorCode(fmt.Sprintf("Unknown (%d)", code))
|
|
}
|
|
|
|
type MailErrorResolveMethod string
|
|
|
|
const (
|
|
MailErrorResolveMethodNone MailErrorResolveMethod = ""
|
|
MailErrorResolveMethodAuth MailErrorResolveMethod = "OAUTH"
|
|
MailErrorResolveMethodRetry MailErrorResolveMethod = "RETRY"
|
|
MailErrorResolveMethodReload MailErrorResolveMethod = "RELOAD"
|
|
)
|
|
|
|
type MailError struct {
|
|
ID string `json:"id"`
|
|
|
|
Type MailErrorType `json:"type"`
|
|
Code MailErrorCode `json:"code"`
|
|
ResolveMethod MailErrorResolveMethod `json:"resolve_method"`
|
|
ResolvedAt *time.Time `json:"resolved_at"`
|
|
|
|
Message string `json:"message"`
|
|
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
func (e *MailError) Error() string {
|
|
return fmt.Sprintf("Email (%s): %s", e.ID, e.Message)
|
|
}
|
|
|
|
func (e *MailError) Unwrap() error {
|
|
return fmt.Errorf("Email (%s): %s", e.ID, e.Message)
|
|
}
|
|
|
|
func MError(eType MailErrorType, code MailErrorCode, message string, resolveMethod MailErrorResolveMethod) *MailError {
|
|
return &MailError{
|
|
ID: uuid.NewString(),
|
|
Type: eType,
|
|
Code: code,
|
|
Message: message,
|
|
ResolveMethod: resolveMethod,
|
|
}
|
|
}
|
|
|
|
var (
|
|
// ErrMailFoldersOverflow is relayed once per worker session when a
|
|
// mailbox has more folders than the sync follows. The inbox and the
|
|
// special folders are always among the ones kept.
|
|
ErrMailFoldersOverflow = func(left int) *MailError {
|
|
return MError(MailErrorWarning, MailErrorCodeFolderLimit, fmt.Sprintf("This mailbox has %d more folders than the %d Warmbly follows. The inbox, sent, drafts, spam and trash are always synced; the rest are taken in the server's order.", left, config.MaxEmailFolders), MailErrorResolveMethodNone)
|
|
}
|
|
// ErrMailFoldersConflict is relayed when the mail server gives two
|
|
// folders the same UIDVALIDITY, which is the id everything downstream
|
|
// identifies a folder by. Only one of them can be followed.
|
|
ErrMailFoldersConflict = func(left int) *MailError {
|
|
return MError(MailErrorWarning, MailErrorCodeFolderConflict, fmt.Sprintf("%d folder(s) on this mailbox share an internal id with another folder, so only one of each pair is synced.", left), MailErrorResolveMethodNone)
|
|
}
|
|
ErrMailUpdateLimit = MError(MailErrorCritical, MailErrorCodeUpdateLimit, "Your inbox has received an unusually large number of updates. Please reactivate your inbox once the issue is resolved.", MailErrorResolveMethodReload)
|
|
ErrMailGoogleAuth = MError(MailErrorCritical, MailErrorCodeGoogleAuth, "Cannot access your Gmail account. Please re-authorize your account to restore mailbox access.", MailErrorResolveMethodReload)
|
|
ErrMailGooglePayment = MError(MailErrorCritical, MailErrorCodeGooglePayment, "Gmail access blocked due to unpaid invoices. Please resolve the payment with Google.", MailErrorResolveMethodReload)
|
|
ErrMailGoogleForbidden = func(message string) *MailError {
|
|
return MError(MailErrorWarning, MailErrorCodeGoogleForbidden, fmt.Sprintf("Gmail access blocked: %s", message), MailErrorResolveMethodReload)
|
|
}
|
|
ErrMailGoogleUnknown = func(code int, message string) *MailError {
|
|
return MError(MailErrorWarning, MailErrorCodeGoogleUnknown(code), message, MailErrorResolveMethodRetry)
|
|
}
|
|
ErrMailServerUnreachable = MError(MailErrorWarning, MailErrorCodeServerUnreachable, "The connection to the mail server could not be established. The server may be offline or blocking the connection.", MailErrorResolveMethodRetry)
|
|
ErrMailResourceNotFound = MError(MailErrorWarning, MailErrorCodeNotFound, "The mail server does not have the folder or message that was requested.", MailErrorResolveMethodRetry)
|
|
ErrMailCondStoreNotSupported = MError(MailErrorCritical, MailErrorCodeUnsupported, "The mail server does not support the required CONDSTORE extension. Synchronization cannot continue.", MailErrorResolveMethodReload)
|
|
ErrMailInvalidCredentials = MError(
|
|
MailErrorCritical,
|
|
MailErrorCodeInvalidCredentials,
|
|
"The email address or password is incorrect. Please check your credentials and try again.",
|
|
MailErrorResolveMethodReload,
|
|
)
|
|
ErrMailAuthenticationFailed = MError(
|
|
MailErrorCritical,
|
|
MailErrorCodeAuthenticationFailed,
|
|
"Authentication failed. This often happens with OAuth2 providers (like Google for Gmail or Microsoft for Outlook). Possible causes: invalid or expired token, wrong permissions/scope, or two-factor authentication requiring an app password. Please re-authenticate or check your account security settings.",
|
|
MailErrorResolveMethodAuth,
|
|
)
|
|
ErrMailAuthorizationFailed = MError(
|
|
MailErrorCritical,
|
|
MailErrorCodeAuthorizationFailed,
|
|
"This account lacks permission to access certain required resources.",
|
|
MailErrorResolveMethodReload,
|
|
)
|
|
ErrMailUnknownImapError = func(errStatus string) *MailError {
|
|
return MError(
|
|
MailErrorCritical,
|
|
MailErrorCodeImapUnknown,
|
|
fmt.Sprintf("Something went wrong: %s", errStatus),
|
|
MailErrorResolveMethodReload,
|
|
)
|
|
}
|
|
|
|
// Rate limiting and abuse errors
|
|
ErrMailRateLimitExceeded = MError(
|
|
MailErrorWarning,
|
|
MailErrorCodeRateLimitExceeded,
|
|
"This email account has exceeded the sync rate limit. This may indicate suspicious activity.",
|
|
MailErrorResolveMethodNone,
|
|
)
|
|
ErrMailSyncFlood = MError(
|
|
MailErrorWarning,
|
|
MailErrorCodeSyncFlood,
|
|
"This mailbox received far more new mail in one hour than sync fair use allows, so syncing was stopped to protect the platform.",
|
|
MailErrorResolveMethodNone,
|
|
)
|
|
ErrMailSyncFairUse = MError(
|
|
MailErrorWarning,
|
|
MailErrorCodeSyncFairUse,
|
|
"This mailbox exceeded its daily sync budget on several recent days, so syncing was stopped. Reactivate it once the volume is back to normal.",
|
|
MailErrorResolveMethodNone,
|
|
)
|
|
ErrMailSendingTooFast = MError(
|
|
MailErrorWarning,
|
|
MailErrorCodeSendingTooFast,
|
|
"Emails are being sent too quickly. Please wait before sending more emails.",
|
|
MailErrorResolveMethodRetry,
|
|
)
|
|
ErrMailRecipientRejected = MError(
|
|
MailErrorWarning,
|
|
MailErrorCodeRecipientRejected,
|
|
"The recipient email address was rejected by the mail server.",
|
|
MailErrorResolveMethodNone,
|
|
)
|
|
ErrMailDomainAuthRejected = MError(
|
|
MailErrorCritical,
|
|
MailErrorCodeDomainAuthRejected,
|
|
"The receiving mail server refused this message because your sending domain failed its authentication checks. Fix the domain's SPF, DKIM and DMARC records.",
|
|
MailErrorResolveMethodNone,
|
|
)
|
|
ErrMailQuotaExceeded = MError(
|
|
MailErrorCritical,
|
|
MailErrorCodeQuotaExceeded,
|
|
"Your email sending quota has been exceeded. Please try again later.",
|
|
MailErrorResolveMethodRetry,
|
|
)
|
|
ErrMailAccountSuspended = MError(
|
|
MailErrorCritical,
|
|
MailErrorCodeAccountSuspended,
|
|
"This email account has been suspended. Please contact your email provider.",
|
|
MailErrorResolveMethodReload,
|
|
)
|
|
)
|
|
|
|
// UserErrorInfo contains user-visible error information
|
|
type UserErrorInfo struct {
|
|
Title string
|
|
Message string
|
|
ActionRequired string
|
|
}
|
|
|
|
// GetUserErrorInfo returns user-friendly error information for display
|
|
func (e *MailError) GetUserErrorInfo() UserErrorInfo {
|
|
info := UserErrorInfo{
|
|
Title: "Email Error",
|
|
Message: e.Message,
|
|
}
|
|
|
|
switch e.Code {
|
|
case MailErrorCodeGoogleAuth, MailErrorCodeAuthenticationFailed:
|
|
info.Title = "Authentication Required"
|
|
info.ActionRequired = "Please re-authorize your email account"
|
|
case MailErrorCodeInvalidCredentials:
|
|
info.Title = "Invalid Credentials"
|
|
info.ActionRequired = "Please update your email credentials"
|
|
case MailErrorCodeServerUnreachable:
|
|
info.Title = "Connection Error"
|
|
info.ActionRequired = "The email server is temporarily unavailable. We'll retry automatically."
|
|
case MailErrorCodeFolderLimit:
|
|
info.Title = "Some folders are not synced"
|
|
info.ActionRequired = "Move or delete folders you no longer need if one you rely on is missing from the unibox."
|
|
case MailErrorCodeFolderConflict:
|
|
info.Title = "Two folders share an internal id"
|
|
info.ActionRequired = "Your mail server gave two folders the same id, so only one of them is synced. Renaming the folder that is missing from the unibox, or recreating it, usually gives it a new one."
|
|
case MailErrorCodeNotFound:
|
|
info.Title = "Mailbox Item Missing"
|
|
info.ActionRequired = "The folder or message is no longer on the mail server. Nothing to do; we'll skip it."
|
|
case MailErrorCodeRateLimitExceeded:
|
|
info.Title = "Rate Limit Exceeded"
|
|
info.ActionRequired = "Your account has been temporarily limited due to unusual activity"
|
|
case MailErrorCodeSyncFlood:
|
|
info.Title = "Sync stopped: unusual volume"
|
|
info.ActionRequired = "Check what is delivering mail into this mailbox, then reactivate it under Mailboxes"
|
|
case MailErrorCodeSyncFairUse:
|
|
info.Title = "Sync stopped: fair use"
|
|
info.ActionRequired = "Reduce the volume landing in this mailbox or ask your administrator to raise the sync budget, then reactivate it"
|
|
case MailErrorCodeSendingTooFast:
|
|
info.Title = "Sending Too Fast"
|
|
info.ActionRequired = "Please wait before sending more emails"
|
|
case MailErrorCodeQuotaExceeded:
|
|
info.Title = "Quota Exceeded"
|
|
info.ActionRequired = "Your daily sending limit has been reached"
|
|
case MailErrorCodeAccountSuspended:
|
|
info.Title = "Account Suspended"
|
|
info.ActionRequired = "Contact your email provider to resolve this issue"
|
|
case MailErrorCodeRecipientRejected:
|
|
info.Title = "Recipient Rejected"
|
|
info.ActionRequired = "The recipient address was not accepted"
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
// IsUserVisible returns true if this error should be shown to users
|
|
func (e *MailError) IsUserVisible() bool {
|
|
return e.Type == MailErrorCritical || e.Type == MailErrorWarning
|
|
}
|