mirror of
https://github.com/hanzoai/mail.git
synced 2026-09-23 23:53:02 +00:00
add mechanism to work around imap client protocol incompatibilities
with a per-account setting to disable imap extensions (capabilities) for sessions of that account. prompted by microsoft office 365, which enables the "UIDONLY" extension (indicating it limits itself to UID-using commands and promising to refrain from using commands based on message sequence numbers (MSN)), but then attempts to execute an old fashioned MSN-based "FETCH" command. users can change the disabled capabilities for their account themselves, requiring no admin intervention to work around the problems, and without affecting any other accounts/users of the system. only the account's imap connections are affected. we won't implement workarounds based on client "ID", as forbidden by its RFC. uidonly-related symptoms reported by FLuX on matrix, thanks! see issue #388 for more information.
This commit is contained in:
+4
-1
@@ -451,7 +451,10 @@ type Account struct {
|
||||
MaxFirstTimeRecipientsPerDay int `sconf:"optional" sconf-doc:"Maximum number of first-time recipients in outgoing messages for this account in a 24 hour window. This limits the damage to recipients and the reputation of this mail server in case of account compromise. Default 200."`
|
||||
NoFirstTimeSenderDelay bool `sconf:"optional" sconf-doc:"Do not apply a delay to SMTP connections before accepting an incoming message from a first-time sender. Can be useful for accounts that sends automated responses and want instant replies."`
|
||||
NoCustomPassword bool `sconf:"optional" sconf-doc:"If set, this account cannot set a password of their own choice, but can only set a new randomly generated password, preventing password reuse across services and use of weak passwords. Custom account passwords can be set by the admin."`
|
||||
Routes []Route `sconf:"optional" sconf-doc:"Routes for delivering outgoing messages through the queue. Each delivery attempt evaluates these account routes, domain routes and finally global routes. The transport of the first matching route is used in the delivery attempt. If no routes match, which is the default with no configured routes, messages are delivered directly from the queue."`
|
||||
IMAPCapabilitiesDisabled []string `sconf:"optional" sconf-doc:"IMAP capabilities (upper-case) to disable on the connection after authentication. Useful if the account uses an email client with an incompatible implementation for a capability/extension."`
|
||||
// We will not work around client incompatibilities based on client software. ../rfc/2971:93
|
||||
|
||||
Routes []Route `sconf:"optional" sconf-doc:"Routes for delivering outgoing messages through the queue. Each delivery attempt evaluates these account routes, domain routes and finally global routes. The transport of the first matching route is used in the delivery attempt. If no routes match, which is the default with no configured routes, messages are delivered directly from the queue."`
|
||||
|
||||
DNSDomain dns.Domain `sconf:"-"` // Parsed form of Domain.
|
||||
JunkMailbox *regexp.Regexp `sconf:"-" json:"-"`
|
||||
|
||||
@@ -1314,6 +1314,12 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
|
||||
# (optional)
|
||||
NoCustomPassword: false
|
||||
|
||||
# IMAP capabilities (upper-case) to disable on the connection after
|
||||
# authentication. Useful if the account uses an email client with an incompatible
|
||||
# implementation for a capability/extension. (optional)
|
||||
IMAPCapabilitiesDisabled:
|
||||
-
|
||||
|
||||
# Routes for delivering outgoing messages through the queue. Each delivery attempt
|
||||
# evaluates these account routes, domain routes and finally global routes. The
|
||||
# transport of the first matching route is used in the delivery attempt. If no
|
||||
|
||||
+32
-3
@@ -137,13 +137,14 @@ var badClientDelay = time.Second // Before reads and after 1-byte writes for pro
|
||||
var authFailDelay = time.Second // After authentication failure.
|
||||
|
||||
// Capabilities (extensions) the server supports. Connections will add a few more,
|
||||
// e.g. STARTTLS, LOGINDISABLED, AUTH=PLAIN.
|
||||
// e.g. STARTTLS, LOGINDISABLED, AUTH=PLAIN. Accounts may disable some
|
||||
// capabilities, to work around compatibility issues.
|
||||
//
|
||||
// We always announce support for SCRAM PLUS-variants, also on connections without
|
||||
// TLS. The client should not be selecting PLUS variants on non-TLS connections,
|
||||
// instead opting to do the bare SCRAM variant without indicating the server claims
|
||||
// to support the PLUS variant (skipping the server downgrade detection check).
|
||||
var serverCapabilities = strings.Join([]string{
|
||||
var serverCapabilitiesList = []string{
|
||||
"IMAP4rev2", // ../rfc/9051
|
||||
"IMAP4rev1", // ../rfc/3501
|
||||
"ENABLE", // ../rfc/5161
|
||||
@@ -186,7 +187,8 @@ var serverCapabilities = strings.Join([]string{
|
||||
"NOTIFY", // ../rfc/5465:195
|
||||
"UIDONLY", // ../rfc/9586:127
|
||||
// "COMPRESS=DEFLATE", // ../rfc/4978, disabled for interoperability issues: The flate reader (inflate) still blocks on partial flushes, preventing progress.
|
||||
}, " ")
|
||||
}
|
||||
var serverCapabilities = strings.Join(serverCapabilitiesList, " ")
|
||||
|
||||
type conn struct {
|
||||
cid int64
|
||||
@@ -2389,6 +2391,19 @@ func (c *conn) cmdCapability(tag, cmd string, p *parser) {
|
||||
// For use in cmdCapability and untagged OK responses on connection start, login and authenticate.
|
||||
func (c *conn) capabilities() string {
|
||||
caps := serverCapabilities
|
||||
if c.account != nil {
|
||||
conf, _ := c.account.Conf()
|
||||
if len(conf.IMAPCapabilitiesDisabled) > 0 {
|
||||
l := make([]string, 0, len(serverCapabilitiesList))
|
||||
for _, cap := range serverCapabilitiesList {
|
||||
if !slices.Contains(conf.IMAPCapabilitiesDisabled, strings.ToUpper(cap)) {
|
||||
l = append(l, cap)
|
||||
}
|
||||
}
|
||||
caps = strings.Join(l, " ")
|
||||
}
|
||||
}
|
||||
|
||||
// ../rfc/9051:1238
|
||||
// We only allow starting without TLS when explicitly configured, in violation of RFC.
|
||||
if !c.tls && c.baseTLSConfig != nil {
|
||||
@@ -3076,8 +3091,22 @@ func (c *conn) cmdEnable(tag, cmd string, p *parser) {
|
||||
// We should only echo that we recognize as needing enabling.
|
||||
var enabled string
|
||||
var qresync bool
|
||||
|
||||
// Accounts can suppress capabilities, we ignore them when the client tries to
|
||||
// enable them.
|
||||
var disabled []string
|
||||
if c.account != nil {
|
||||
conf, _ := c.account.Conf()
|
||||
disabled = conf.IMAPCapabilitiesDisabled
|
||||
}
|
||||
|
||||
for _, s := range caps {
|
||||
cap := capability(strings.ToUpper(s))
|
||||
|
||||
if slices.Contains(disabled, string(cap)) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch cap {
|
||||
case capIMAP4rev2,
|
||||
capUTF8Accept,
|
||||
|
||||
@@ -982,3 +982,37 @@ func TestReference(t *testing.T) {
|
||||
tc2.transactf("ok", "fetch 1 rfc822.size")
|
||||
tc2.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchRFC822Size(len(exampleMsg))))
|
||||
}
|
||||
|
||||
// Test that disabled capabilities aren't announced after authentication, and can't be enabled.
|
||||
func TestCapabilitiesDisabled(t *testing.T) {
|
||||
tc := start(t, false)
|
||||
defer tc.close()
|
||||
|
||||
// Set password for account.
|
||||
acc, err := store.OpenAccount(pkglog, "imapcaps", false)
|
||||
tcheck(t, err, "open account")
|
||||
err = acc.SetPassword(pkglog, password0)
|
||||
tcheck(t, err, "set password")
|
||||
|
||||
var caps, capsx []imapclient.Capability
|
||||
for _, s := range serverCapabilitiesList {
|
||||
s = strings.ToUpper(s)
|
||||
caps = append(caps, imapclient.Capability(s))
|
||||
if s != "UIDONLY" {
|
||||
capsx = append(capsx, imapclient.Capability(s))
|
||||
}
|
||||
}
|
||||
caps = append(caps, "STARTTLS", "AUTH=PLAIN")
|
||||
capsx = append(capsx, "STARTTLS", "AUTH=PLAIN")
|
||||
|
||||
// Initially, all transactions are announced.
|
||||
tc.transactf("ok", "capability")
|
||||
tc.xuntagged(imapclient.UntaggedCapability(caps))
|
||||
|
||||
// After login, UIDONLY is disabled for the account.
|
||||
tc.login("[email protected]", password0)
|
||||
tc.transactf("ok", "capability")
|
||||
tc.xuntagged(imapclient.UntaggedCapability(capsx))
|
||||
tc.transactf("ok", "enable condstore uidonly")
|
||||
tc.xuntagged(imapclient.UntaggedEnabled{imapclient.CapCondstore}) // Not UIDONLY.
|
||||
}
|
||||
|
||||
Vendored
+6
@@ -40,3 +40,9 @@ Accounts:
|
||||
LoginDisabled: testing
|
||||
Destinations:
|
||||
[email protected]: nil
|
||||
imapcaps:
|
||||
Domain: mox.example
|
||||
Destinations:
|
||||
[email protected]: nil
|
||||
IMAPCapabilitiesDisabled:
|
||||
- UIDONLY
|
||||
|
||||
@@ -803,3 +803,23 @@ func (Account) LoginAttempts(ctx context.Context, limit int) []store.LoginAttemp
|
||||
xcheckf(ctx, err, "listing login attempts")
|
||||
return l
|
||||
}
|
||||
|
||||
func (Account) IMAPSave(ctx context.Context, capabilitiesDisabled []string) {
|
||||
// Basic check for capabilities.
|
||||
for _, s := range capabilitiesDisabled {
|
||||
if strings.ToUpper(s) != s {
|
||||
xcheckuserf(ctx, errors.New("capability must be in upper case"), "checking capabilities")
|
||||
}
|
||||
for _, c := range s {
|
||||
if c <= ' ' {
|
||||
xcheckuserf(ctx, errors.New("capability cannot contain control characters"), "checking capabilities")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
|
||||
err := admin.AccountSave(ctx, reqInfo.AccountName, func(acc *config.Account) {
|
||||
acc.IMAPCapabilitiesDisabled = capabilitiesDisabled
|
||||
})
|
||||
xcheckf(ctx, err, "saving disabled imap capabilities")
|
||||
}
|
||||
|
||||
+15
-2
@@ -272,7 +272,7 @@ var api;
|
||||
api.stringsTypes = { "AuthResult": true, "CSRFToken": true, "Localpart": true, "OutgoingEvent": true };
|
||||
api.intsTypes = {};
|
||||
api.types = {
|
||||
"Account": { "Name": "Account", "Docs": "", "Fields": [{ "Name": "OutgoingWebhook", "Docs": "", "Typewords": ["nullable", "OutgoingWebhook"] }, { "Name": "IncomingWebhook", "Docs": "", "Typewords": ["nullable", "IncomingWebhook"] }, { "Name": "FromIDLoginAddresses", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "KeepRetiredMessagePeriod", "Docs": "", "Typewords": ["int64"] }, { "Name": "KeepRetiredWebhookPeriod", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginDisabled", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["string"] }, { "Name": "Description", "Docs": "", "Typewords": ["string"] }, { "Name": "FullName", "Docs": "", "Typewords": ["string"] }, { "Name": "Destinations", "Docs": "", "Typewords": ["{}", "Destination"] }, { "Name": "SubjectPass", "Docs": "", "Typewords": ["SubjectPass"] }, { "Name": "QuotaMessageSize", "Docs": "", "Typewords": ["int64"] }, { "Name": "RejectsMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "KeepRejects", "Docs": "", "Typewords": ["bool"] }, { "Name": "AutomaticJunkFlags", "Docs": "", "Typewords": ["AutomaticJunkFlags"] }, { "Name": "JunkFilter", "Docs": "", "Typewords": ["nullable", "JunkFilter"] }, { "Name": "MaxOutgoingMessagesPerDay", "Docs": "", "Typewords": ["int32"] }, { "Name": "MaxFirstTimeRecipientsPerDay", "Docs": "", "Typewords": ["int32"] }, { "Name": "NoFirstTimeSenderDelay", "Docs": "", "Typewords": ["bool"] }, { "Name": "NoCustomPassword", "Docs": "", "Typewords": ["bool"] }, { "Name": "Routes", "Docs": "", "Typewords": ["[]", "Route"] }, { "Name": "DNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "Aliases", "Docs": "", "Typewords": ["[]", "AddressAlias"] }] },
|
||||
"Account": { "Name": "Account", "Docs": "", "Fields": [{ "Name": "OutgoingWebhook", "Docs": "", "Typewords": ["nullable", "OutgoingWebhook"] }, { "Name": "IncomingWebhook", "Docs": "", "Typewords": ["nullable", "IncomingWebhook"] }, { "Name": "FromIDLoginAddresses", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "KeepRetiredMessagePeriod", "Docs": "", "Typewords": ["int64"] }, { "Name": "KeepRetiredWebhookPeriod", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginDisabled", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["string"] }, { "Name": "Description", "Docs": "", "Typewords": ["string"] }, { "Name": "FullName", "Docs": "", "Typewords": ["string"] }, { "Name": "Destinations", "Docs": "", "Typewords": ["{}", "Destination"] }, { "Name": "SubjectPass", "Docs": "", "Typewords": ["SubjectPass"] }, { "Name": "QuotaMessageSize", "Docs": "", "Typewords": ["int64"] }, { "Name": "RejectsMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "KeepRejects", "Docs": "", "Typewords": ["bool"] }, { "Name": "AutomaticJunkFlags", "Docs": "", "Typewords": ["AutomaticJunkFlags"] }, { "Name": "JunkFilter", "Docs": "", "Typewords": ["nullable", "JunkFilter"] }, { "Name": "MaxOutgoingMessagesPerDay", "Docs": "", "Typewords": ["int32"] }, { "Name": "MaxFirstTimeRecipientsPerDay", "Docs": "", "Typewords": ["int32"] }, { "Name": "NoFirstTimeSenderDelay", "Docs": "", "Typewords": ["bool"] }, { "Name": "NoCustomPassword", "Docs": "", "Typewords": ["bool"] }, { "Name": "IMAPCapabilitiesDisabled", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Routes", "Docs": "", "Typewords": ["[]", "Route"] }, { "Name": "DNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "Aliases", "Docs": "", "Typewords": ["[]", "AddressAlias"] }] },
|
||||
"OutgoingWebhook": { "Name": "OutgoingWebhook", "Docs": "", "Fields": [{ "Name": "URL", "Docs": "", "Typewords": ["string"] }, { "Name": "Authorization", "Docs": "", "Typewords": ["string"] }, { "Name": "Events", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||
"IncomingWebhook": { "Name": "IncomingWebhook", "Docs": "", "Fields": [{ "Name": "URL", "Docs": "", "Typewords": ["string"] }, { "Name": "Authorization", "Docs": "", "Typewords": ["string"] }] },
|
||||
"Destination": { "Name": "Destination", "Docs": "", "Fields": [{ "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Rulesets", "Docs": "", "Typewords": ["[]", "Ruleset"] }, { "Name": "SMTPError", "Docs": "", "Typewords": ["string"] }, { "Name": "MessageAuthRequiredSMTPError", "Docs": "", "Typewords": ["string"] }, { "Name": "FullName", "Docs": "", "Typewords": ["string"] }] },
|
||||
@@ -603,6 +603,13 @@ var api;
|
||||
const params = [limit];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
async IMAPSave(capabilitiesDisabled) {
|
||||
const fn = "IMAPSave";
|
||||
const paramTypes = [["[]", "string"]];
|
||||
const returnTypes = [];
|
||||
const params = [capabilitiesDisabled];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
}
|
||||
api.Client = Client;
|
||||
api.defaultBaseURL = (function () {
|
||||
@@ -1276,6 +1283,8 @@ const index = async () => {
|
||||
let importAbortBox;
|
||||
let suppressionAddress;
|
||||
let suppressionReason;
|
||||
let imapFieldset;
|
||||
let imapCapabilitiesDisabled;
|
||||
const importTrack = async (token) => {
|
||||
const importConnection = dom.div('Waiting for updates...');
|
||||
importProgress.appendChild(importConnection);
|
||||
@@ -1714,7 +1723,11 @@ openssl pkcs12 \\
|
||||
}), dom.table(dom.thead(dom.tr(dom.th('Address', attr.title('Address that caused this entry to be added to the list. The title (shown on hover) displays an address with a fictional simplified localpart, with lower-cased, dots removed, only first part before "+" or "-" (typicaly catchall separators). When checking if an address is on the suppression list, it is checked against this address.')), dom.th('Manual', attr.title('Whether suppression was added manually, instead of automatically based on bounces.')), dom.th('Reason'), dom.th('Since'), dom.th('Action'))), dom.tbody((suppressions || []).length === 0 ? dom.tr(dom.td(attr.colspan('5'), '(None)')) : [], (suppressions || []).map(s => dom.tr(dom.td(prewrap(s.OriginalAddress), attr.title(s.BaseAddress)), dom.td(s.Manual ? '✓' : ''), dom.td(s.Reason), dom.td(age(s.Created)), dom.td(dom.clickbutton('Remove', async function click(e) {
|
||||
await check(e.target, client.SuppressionRemove(s.OriginalAddress));
|
||||
window.location.reload(); // todo: reload less
|
||||
}))))), dom.tfoot(dom.tr(dom.td(suppressionAddress = dom.input(attr.type('required'), attr.form('suppressionAdd'))), dom.td(), dom.td(suppressionReason = dom.input(style({ width: '100%' }), attr.form('suppressionAdd'))), dom.td(), dom.td(dom.submitbutton('Add suppression', attr.form('suppressionAdd')))))), dom.br(), dom.h2('Export'), dom.p('Export all messages in all mailboxes.'), dom.form(attr.target('_blank'), attr.method('POST'), attr.action('export'), dom.input(attr.type('hidden'), attr.name('csrf'), attr.value(localStorageGet('webaccountcsrftoken') || '')), dom.input(attr.type('hidden'), attr.name('mailbox'), attr.value('')), dom.input(attr.type('hidden'), attr.name('recursive'), attr.value('on')), dom.div(style({ display: 'flex', flexDirection: 'column', gap: '.5ex' }), dom.div(dom.label(dom.input(attr.type('radio'), attr.name('format'), attr.value('maildir'), attr.checked('')), ' Maildir'), ' ', dom.label(dom.input(attr.type('radio'), attr.name('format'), attr.value('mbox')), ' Mbox')), dom.div(dom.label(dom.input(attr.type('radio'), attr.name('archive'), attr.value('tar')), ' Tar'), ' ', dom.label(dom.input(attr.type('radio'), attr.name('archive'), attr.value('tgz'), attr.checked('')), ' Tgz'), ' ', dom.label(dom.input(attr.type('radio'), attr.name('archive'), attr.value('zip')), ' Zip'), ' '), dom.div(style({ marginTop: '1ex' }), dom.submitbutton('Export')))), dom.br(), dom.h2('Import'), dom.p('Import messages from a .zip or .tgz file with maildirs and/or mbox files.'), importForm = dom.form(async function submit(e) {
|
||||
}))))), dom.tfoot(dom.tr(dom.td(suppressionAddress = dom.input(attr.type('required'), attr.form('suppressionAdd'))), dom.td(), dom.td(suppressionReason = dom.input(style({ width: '100%' }), attr.form('suppressionAdd'))), dom.td(), dom.td(dom.submitbutton('Add suppression', attr.form('suppressionAdd')))))), dom.br(), dom.h2('IMAP'), dom.form(async function submit(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
await check(imapFieldset, (async () => await client.IMAPSave(imapCapabilitiesDisabled.value.split(' ').filter(s => s)))());
|
||||
}, imapFieldset = dom.fieldset(dom.div(style({ display: 'flex', gap: '1em', alignItems: 'flex-end' }), dom.div(dom.label('Disabled IMAP capabilities (space-separated)', attr.title('IMAP capabilities (upper-case) to disable on the connection after authentication. Useful if the account uses an email client with an incompatible implementation for a capability/extension.'), dom.br(), imapCapabilitiesDisabled = dom.input(attr.value((acc.IMAPCapabilitiesDisabled || []).join(' '))))), dom.div(dom.submitbutton('Save'))))), dom.br(), dom.h2('Export'), dom.p('Export all messages in all mailboxes.'), dom.form(attr.target('_blank'), attr.method('POST'), attr.action('export'), dom.input(attr.type('hidden'), attr.name('csrf'), attr.value(localStorageGet('webaccountcsrftoken') || '')), dom.input(attr.type('hidden'), attr.name('mailbox'), attr.value('')), dom.input(attr.type('hidden'), attr.name('recursive'), attr.value('on')), dom.div(style({ display: 'flex', flexDirection: 'column', gap: '.5ex' }), dom.div(dom.label(dom.input(attr.type('radio'), attr.name('format'), attr.value('maildir'), attr.checked('')), ' Maildir'), ' ', dom.label(dom.input(attr.type('radio'), attr.name('format'), attr.value('mbox')), ' Mbox')), dom.div(dom.label(dom.input(attr.type('radio'), attr.name('archive'), attr.value('tar')), ' Tar'), ' ', dom.label(dom.input(attr.type('radio'), attr.name('archive'), attr.value('tgz'), attr.checked('')), ' Tgz'), ' ', dom.label(dom.input(attr.type('radio'), attr.name('archive'), attr.value('zip')), ' Zip'), ' '), dom.div(style({ marginTop: '1ex' }), dom.submitbutton('Export')))), dom.br(), dom.h2('Import'), dom.p('Import messages from a .zip or .tgz file with maildirs and/or mbox files.'), importForm = dom.form(async function submit(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const request = async () => {
|
||||
|
||||
@@ -395,6 +395,9 @@ const index = async () => {
|
||||
let suppressionAddress: HTMLInputElement
|
||||
let suppressionReason: HTMLInputElement
|
||||
|
||||
let imapFieldset: HTMLFieldSetElement
|
||||
let imapCapabilitiesDisabled: HTMLInputElement
|
||||
|
||||
const importTrack = async (token: string) => {
|
||||
const importConnection = dom.div('Waiting for updates...')
|
||||
importProgress.appendChild(importConnection)
|
||||
@@ -1480,6 +1483,33 @@ openssl pkcs12 \\
|
||||
),
|
||||
dom.br(),
|
||||
|
||||
dom.h2('IMAP'),
|
||||
dom.form(
|
||||
async function submit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
await check(imapFieldset, (async () => await client.IMAPSave(imapCapabilitiesDisabled.value.split(' ').filter(s => s)))())
|
||||
},
|
||||
imapFieldset=dom.fieldset(
|
||||
dom.div(
|
||||
style({display: 'flex', gap: '1em', alignItems: 'flex-end'}),
|
||||
dom.div(
|
||||
dom.label(
|
||||
'Disabled IMAP capabilities (space-separated)',
|
||||
attr.title('IMAP capabilities (upper-case) to disable on the connection after authentication. Useful if the account uses an email client with an incompatible implementation for a capability/extension.'),
|
||||
dom.br(),
|
||||
imapCapabilitiesDisabled=dom.input(attr.value((acc.IMAPCapabilitiesDisabled || []).join(' '))),
|
||||
),
|
||||
),
|
||||
dom.div(
|
||||
dom.submitbutton('Save'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
dom.br(),
|
||||
|
||||
dom.h2('Export'),
|
||||
dom.p('Export all messages in all mailboxes.'),
|
||||
dom.form(
|
||||
|
||||
@@ -551,6 +551,14 @@ func TestAccount(t *testing.T) {
|
||||
tcheck(t, err, "list tls public keys")
|
||||
tcompare(t, len(tpkl), 0)
|
||||
|
||||
tneedErrorCode(t, "user:error", func() { api.IMAPSave(ctx, []string{"BAD\nBAD"}) })
|
||||
api.IMAPSave(ctx, []string{"UIDONLY"})
|
||||
account, _, _, _ = api.Account(ctx)
|
||||
tcompare(t, account.IMAPCapabilitiesDisabled, []string{"UIDONLY"})
|
||||
api.IMAPSave(ctx, []string{})
|
||||
account, _, _, _ = api.Account(ctx)
|
||||
tcompare(t, account.IMAPCapabilitiesDisabled, []string{})
|
||||
|
||||
api.Logout(ctx)
|
||||
tneedErrorCode(t, "server:error", func() { api.Logout(ctx) })
|
||||
}
|
||||
|
||||
@@ -587,6 +587,20 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "IMAPSave",
|
||||
"Docs": "",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "capabilitiesDisabled",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"string"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": []
|
||||
}
|
||||
],
|
||||
"Sections": [],
|
||||
@@ -740,6 +754,14 @@
|
||||
"bool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "IMAPCapabilitiesDisabled",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Routes",
|
||||
"Docs": "",
|
||||
|
||||
+10
-1
@@ -23,6 +23,7 @@ export interface Account {
|
||||
MaxFirstTimeRecipientsPerDay: number
|
||||
NoFirstTimeSenderDelay: boolean
|
||||
NoCustomPassword: boolean
|
||||
IMAPCapabilitiesDisabled?: string[] | null
|
||||
Routes?: Route[] | null
|
||||
DNSDomain: Domain // Parsed form of Domain.
|
||||
Aliases?: AddressAlias[] | null
|
||||
@@ -296,7 +297,7 @@ export const structTypes: {[typename: string]: boolean} = {"Account":true,"Addre
|
||||
export const stringsTypes: {[typename: string]: boolean} = {"AuthResult":true,"CSRFToken":true,"Localpart":true,"OutgoingEvent":true}
|
||||
export const intsTypes: {[typename: string]: boolean} = {}
|
||||
export const types: TypenameMap = {
|
||||
"Account": {"Name":"Account","Docs":"","Fields":[{"Name":"OutgoingWebhook","Docs":"","Typewords":["nullable","OutgoingWebhook"]},{"Name":"IncomingWebhook","Docs":"","Typewords":["nullable","IncomingWebhook"]},{"Name":"FromIDLoginAddresses","Docs":"","Typewords":["[]","string"]},{"Name":"KeepRetiredMessagePeriod","Docs":"","Typewords":["int64"]},{"Name":"KeepRetiredWebhookPeriod","Docs":"","Typewords":["int64"]},{"Name":"LoginDisabled","Docs":"","Typewords":["string"]},{"Name":"Domain","Docs":"","Typewords":["string"]},{"Name":"Description","Docs":"","Typewords":["string"]},{"Name":"FullName","Docs":"","Typewords":["string"]},{"Name":"Destinations","Docs":"","Typewords":["{}","Destination"]},{"Name":"SubjectPass","Docs":"","Typewords":["SubjectPass"]},{"Name":"QuotaMessageSize","Docs":"","Typewords":["int64"]},{"Name":"RejectsMailbox","Docs":"","Typewords":["string"]},{"Name":"KeepRejects","Docs":"","Typewords":["bool"]},{"Name":"AutomaticJunkFlags","Docs":"","Typewords":["AutomaticJunkFlags"]},{"Name":"JunkFilter","Docs":"","Typewords":["nullable","JunkFilter"]},{"Name":"MaxOutgoingMessagesPerDay","Docs":"","Typewords":["int32"]},{"Name":"MaxFirstTimeRecipientsPerDay","Docs":"","Typewords":["int32"]},{"Name":"NoFirstTimeSenderDelay","Docs":"","Typewords":["bool"]},{"Name":"NoCustomPassword","Docs":"","Typewords":["bool"]},{"Name":"Routes","Docs":"","Typewords":["[]","Route"]},{"Name":"DNSDomain","Docs":"","Typewords":["Domain"]},{"Name":"Aliases","Docs":"","Typewords":["[]","AddressAlias"]}]},
|
||||
"Account": {"Name":"Account","Docs":"","Fields":[{"Name":"OutgoingWebhook","Docs":"","Typewords":["nullable","OutgoingWebhook"]},{"Name":"IncomingWebhook","Docs":"","Typewords":["nullable","IncomingWebhook"]},{"Name":"FromIDLoginAddresses","Docs":"","Typewords":["[]","string"]},{"Name":"KeepRetiredMessagePeriod","Docs":"","Typewords":["int64"]},{"Name":"KeepRetiredWebhookPeriod","Docs":"","Typewords":["int64"]},{"Name":"LoginDisabled","Docs":"","Typewords":["string"]},{"Name":"Domain","Docs":"","Typewords":["string"]},{"Name":"Description","Docs":"","Typewords":["string"]},{"Name":"FullName","Docs":"","Typewords":["string"]},{"Name":"Destinations","Docs":"","Typewords":["{}","Destination"]},{"Name":"SubjectPass","Docs":"","Typewords":["SubjectPass"]},{"Name":"QuotaMessageSize","Docs":"","Typewords":["int64"]},{"Name":"RejectsMailbox","Docs":"","Typewords":["string"]},{"Name":"KeepRejects","Docs":"","Typewords":["bool"]},{"Name":"AutomaticJunkFlags","Docs":"","Typewords":["AutomaticJunkFlags"]},{"Name":"JunkFilter","Docs":"","Typewords":["nullable","JunkFilter"]},{"Name":"MaxOutgoingMessagesPerDay","Docs":"","Typewords":["int32"]},{"Name":"MaxFirstTimeRecipientsPerDay","Docs":"","Typewords":["int32"]},{"Name":"NoFirstTimeSenderDelay","Docs":"","Typewords":["bool"]},{"Name":"NoCustomPassword","Docs":"","Typewords":["bool"]},{"Name":"IMAPCapabilitiesDisabled","Docs":"","Typewords":["[]","string"]},{"Name":"Routes","Docs":"","Typewords":["[]","Route"]},{"Name":"DNSDomain","Docs":"","Typewords":["Domain"]},{"Name":"Aliases","Docs":"","Typewords":["[]","AddressAlias"]}]},
|
||||
"OutgoingWebhook": {"Name":"OutgoingWebhook","Docs":"","Fields":[{"Name":"URL","Docs":"","Typewords":["string"]},{"Name":"Authorization","Docs":"","Typewords":["string"]},{"Name":"Events","Docs":"","Typewords":["[]","string"]}]},
|
||||
"IncomingWebhook": {"Name":"IncomingWebhook","Docs":"","Fields":[{"Name":"URL","Docs":"","Typewords":["string"]},{"Name":"Authorization","Docs":"","Typewords":["string"]}]},
|
||||
"Destination": {"Name":"Destination","Docs":"","Fields":[{"Name":"Mailbox","Docs":"","Typewords":["string"]},{"Name":"Rulesets","Docs":"","Typewords":["[]","Ruleset"]},{"Name":"SMTPError","Docs":"","Typewords":["string"]},{"Name":"MessageAuthRequiredSMTPError","Docs":"","Typewords":["string"]},{"Name":"FullName","Docs":"","Typewords":["string"]}]},
|
||||
@@ -661,6 +662,14 @@ export class Client {
|
||||
const params: any[] = [limit]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as LoginAttempt[] | null
|
||||
}
|
||||
|
||||
async IMAPSave(capabilitiesDisabled: string[] | null): Promise<void> {
|
||||
const fn: string = "IMAPSave"
|
||||
const paramTypes: string[][] = [["[]","string"]]
|
||||
const returnTypes: string[][] = []
|
||||
const params: any[] = [capabilitiesDisabled]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultBaseURL = (function() {
|
||||
|
||||
+1
-1
@@ -311,7 +311,7 @@ var api;
|
||||
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Localpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||
"Destination": { "Name": "Destination", "Docs": "", "Fields": [{ "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Rulesets", "Docs": "", "Typewords": ["[]", "Ruleset"] }, { "Name": "SMTPError", "Docs": "", "Typewords": ["string"] }, { "Name": "MessageAuthRequiredSMTPError", "Docs": "", "Typewords": ["string"] }, { "Name": "FullName", "Docs": "", "Typewords": ["string"] }] },
|
||||
"Ruleset": { "Name": "Ruleset", "Docs": "", "Fields": [{ "Name": "SMTPMailFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "HeadersRegexp", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListAllowDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "AcceptRejectsToMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Comment", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "ListAllowDNSDomain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||
"Account": { "Name": "Account", "Docs": "", "Fields": [{ "Name": "OutgoingWebhook", "Docs": "", "Typewords": ["nullable", "OutgoingWebhook"] }, { "Name": "IncomingWebhook", "Docs": "", "Typewords": ["nullable", "IncomingWebhook"] }, { "Name": "FromIDLoginAddresses", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "KeepRetiredMessagePeriod", "Docs": "", "Typewords": ["int64"] }, { "Name": "KeepRetiredWebhookPeriod", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginDisabled", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["string"] }, { "Name": "Description", "Docs": "", "Typewords": ["string"] }, { "Name": "FullName", "Docs": "", "Typewords": ["string"] }, { "Name": "Destinations", "Docs": "", "Typewords": ["{}", "Destination"] }, { "Name": "SubjectPass", "Docs": "", "Typewords": ["SubjectPass"] }, { "Name": "QuotaMessageSize", "Docs": "", "Typewords": ["int64"] }, { "Name": "RejectsMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "KeepRejects", "Docs": "", "Typewords": ["bool"] }, { "Name": "AutomaticJunkFlags", "Docs": "", "Typewords": ["AutomaticJunkFlags"] }, { "Name": "JunkFilter", "Docs": "", "Typewords": ["nullable", "JunkFilter"] }, { "Name": "MaxOutgoingMessagesPerDay", "Docs": "", "Typewords": ["int32"] }, { "Name": "MaxFirstTimeRecipientsPerDay", "Docs": "", "Typewords": ["int32"] }, { "Name": "NoFirstTimeSenderDelay", "Docs": "", "Typewords": ["bool"] }, { "Name": "NoCustomPassword", "Docs": "", "Typewords": ["bool"] }, { "Name": "Routes", "Docs": "", "Typewords": ["[]", "Route"] }, { "Name": "DNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "Aliases", "Docs": "", "Typewords": ["[]", "AddressAlias"] }] },
|
||||
"Account": { "Name": "Account", "Docs": "", "Fields": [{ "Name": "OutgoingWebhook", "Docs": "", "Typewords": ["nullable", "OutgoingWebhook"] }, { "Name": "IncomingWebhook", "Docs": "", "Typewords": ["nullable", "IncomingWebhook"] }, { "Name": "FromIDLoginAddresses", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "KeepRetiredMessagePeriod", "Docs": "", "Typewords": ["int64"] }, { "Name": "KeepRetiredWebhookPeriod", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginDisabled", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["string"] }, { "Name": "Description", "Docs": "", "Typewords": ["string"] }, { "Name": "FullName", "Docs": "", "Typewords": ["string"] }, { "Name": "Destinations", "Docs": "", "Typewords": ["{}", "Destination"] }, { "Name": "SubjectPass", "Docs": "", "Typewords": ["SubjectPass"] }, { "Name": "QuotaMessageSize", "Docs": "", "Typewords": ["int64"] }, { "Name": "RejectsMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "KeepRejects", "Docs": "", "Typewords": ["bool"] }, { "Name": "AutomaticJunkFlags", "Docs": "", "Typewords": ["AutomaticJunkFlags"] }, { "Name": "JunkFilter", "Docs": "", "Typewords": ["nullable", "JunkFilter"] }, { "Name": "MaxOutgoingMessagesPerDay", "Docs": "", "Typewords": ["int32"] }, { "Name": "MaxFirstTimeRecipientsPerDay", "Docs": "", "Typewords": ["int32"] }, { "Name": "NoFirstTimeSenderDelay", "Docs": "", "Typewords": ["bool"] }, { "Name": "NoCustomPassword", "Docs": "", "Typewords": ["bool"] }, { "Name": "IMAPCapabilitiesDisabled", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Routes", "Docs": "", "Typewords": ["[]", "Route"] }, { "Name": "DNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "Aliases", "Docs": "", "Typewords": ["[]", "AddressAlias"] }] },
|
||||
"OutgoingWebhook": { "Name": "OutgoingWebhook", "Docs": "", "Fields": [{ "Name": "URL", "Docs": "", "Typewords": ["string"] }, { "Name": "Authorization", "Docs": "", "Typewords": ["string"] }, { "Name": "Events", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||
"IncomingWebhook": { "Name": "IncomingWebhook", "Docs": "", "Fields": [{ "Name": "URL", "Docs": "", "Typewords": ["string"] }, { "Name": "Authorization", "Docs": "", "Typewords": ["string"] }] },
|
||||
"SubjectPass": { "Name": "SubjectPass", "Docs": "", "Fields": [{ "Name": "Period", "Docs": "", "Typewords": ["int64"] }] },
|
||||
|
||||
@@ -4221,6 +4221,14 @@
|
||||
"bool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "IMAPCapabilitiesDisabled",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Routes",
|
||||
"Docs": "",
|
||||
|
||||
+2
-1
@@ -403,6 +403,7 @@ export interface Account {
|
||||
MaxFirstTimeRecipientsPerDay: number
|
||||
NoFirstTimeSenderDelay: boolean
|
||||
NoCustomPassword: boolean
|
||||
IMAPCapabilitiesDisabled?: string[] | null
|
||||
Routes?: Route[] | null
|
||||
DNSDomain: Domain // Parsed form of Domain.
|
||||
Aliases?: AddressAlias[] | null
|
||||
@@ -1208,7 +1209,7 @@ export const types: TypenameMap = {
|
||||
"Address": {"Name":"Address","Docs":"","Fields":[{"Name":"Localpart","Docs":"","Typewords":["Localpart"]},{"Name":"Domain","Docs":"","Typewords":["Domain"]}]},
|
||||
"Destination": {"Name":"Destination","Docs":"","Fields":[{"Name":"Mailbox","Docs":"","Typewords":["string"]},{"Name":"Rulesets","Docs":"","Typewords":["[]","Ruleset"]},{"Name":"SMTPError","Docs":"","Typewords":["string"]},{"Name":"MessageAuthRequiredSMTPError","Docs":"","Typewords":["string"]},{"Name":"FullName","Docs":"","Typewords":["string"]}]},
|
||||
"Ruleset": {"Name":"Ruleset","Docs":"","Fields":[{"Name":"SMTPMailFromRegexp","Docs":"","Typewords":["string"]},{"Name":"MsgFromRegexp","Docs":"","Typewords":["string"]},{"Name":"VerifiedDomain","Docs":"","Typewords":["string"]},{"Name":"HeadersRegexp","Docs":"","Typewords":["{}","string"]},{"Name":"IsForward","Docs":"","Typewords":["bool"]},{"Name":"ListAllowDomain","Docs":"","Typewords":["string"]},{"Name":"AcceptRejectsToMailbox","Docs":"","Typewords":["string"]},{"Name":"Mailbox","Docs":"","Typewords":["string"]},{"Name":"Comment","Docs":"","Typewords":["string"]},{"Name":"VerifiedDNSDomain","Docs":"","Typewords":["Domain"]},{"Name":"ListAllowDNSDomain","Docs":"","Typewords":["Domain"]}]},
|
||||
"Account": {"Name":"Account","Docs":"","Fields":[{"Name":"OutgoingWebhook","Docs":"","Typewords":["nullable","OutgoingWebhook"]},{"Name":"IncomingWebhook","Docs":"","Typewords":["nullable","IncomingWebhook"]},{"Name":"FromIDLoginAddresses","Docs":"","Typewords":["[]","string"]},{"Name":"KeepRetiredMessagePeriod","Docs":"","Typewords":["int64"]},{"Name":"KeepRetiredWebhookPeriod","Docs":"","Typewords":["int64"]},{"Name":"LoginDisabled","Docs":"","Typewords":["string"]},{"Name":"Domain","Docs":"","Typewords":["string"]},{"Name":"Description","Docs":"","Typewords":["string"]},{"Name":"FullName","Docs":"","Typewords":["string"]},{"Name":"Destinations","Docs":"","Typewords":["{}","Destination"]},{"Name":"SubjectPass","Docs":"","Typewords":["SubjectPass"]},{"Name":"QuotaMessageSize","Docs":"","Typewords":["int64"]},{"Name":"RejectsMailbox","Docs":"","Typewords":["string"]},{"Name":"KeepRejects","Docs":"","Typewords":["bool"]},{"Name":"AutomaticJunkFlags","Docs":"","Typewords":["AutomaticJunkFlags"]},{"Name":"JunkFilter","Docs":"","Typewords":["nullable","JunkFilter"]},{"Name":"MaxOutgoingMessagesPerDay","Docs":"","Typewords":["int32"]},{"Name":"MaxFirstTimeRecipientsPerDay","Docs":"","Typewords":["int32"]},{"Name":"NoFirstTimeSenderDelay","Docs":"","Typewords":["bool"]},{"Name":"NoCustomPassword","Docs":"","Typewords":["bool"]},{"Name":"Routes","Docs":"","Typewords":["[]","Route"]},{"Name":"DNSDomain","Docs":"","Typewords":["Domain"]},{"Name":"Aliases","Docs":"","Typewords":["[]","AddressAlias"]}]},
|
||||
"Account": {"Name":"Account","Docs":"","Fields":[{"Name":"OutgoingWebhook","Docs":"","Typewords":["nullable","OutgoingWebhook"]},{"Name":"IncomingWebhook","Docs":"","Typewords":["nullable","IncomingWebhook"]},{"Name":"FromIDLoginAddresses","Docs":"","Typewords":["[]","string"]},{"Name":"KeepRetiredMessagePeriod","Docs":"","Typewords":["int64"]},{"Name":"KeepRetiredWebhookPeriod","Docs":"","Typewords":["int64"]},{"Name":"LoginDisabled","Docs":"","Typewords":["string"]},{"Name":"Domain","Docs":"","Typewords":["string"]},{"Name":"Description","Docs":"","Typewords":["string"]},{"Name":"FullName","Docs":"","Typewords":["string"]},{"Name":"Destinations","Docs":"","Typewords":["{}","Destination"]},{"Name":"SubjectPass","Docs":"","Typewords":["SubjectPass"]},{"Name":"QuotaMessageSize","Docs":"","Typewords":["int64"]},{"Name":"RejectsMailbox","Docs":"","Typewords":["string"]},{"Name":"KeepRejects","Docs":"","Typewords":["bool"]},{"Name":"AutomaticJunkFlags","Docs":"","Typewords":["AutomaticJunkFlags"]},{"Name":"JunkFilter","Docs":"","Typewords":["nullable","JunkFilter"]},{"Name":"MaxOutgoingMessagesPerDay","Docs":"","Typewords":["int32"]},{"Name":"MaxFirstTimeRecipientsPerDay","Docs":"","Typewords":["int32"]},{"Name":"NoFirstTimeSenderDelay","Docs":"","Typewords":["bool"]},{"Name":"NoCustomPassword","Docs":"","Typewords":["bool"]},{"Name":"IMAPCapabilitiesDisabled","Docs":"","Typewords":["[]","string"]},{"Name":"Routes","Docs":"","Typewords":["[]","Route"]},{"Name":"DNSDomain","Docs":"","Typewords":["Domain"]},{"Name":"Aliases","Docs":"","Typewords":["[]","AddressAlias"]}]},
|
||||
"OutgoingWebhook": {"Name":"OutgoingWebhook","Docs":"","Fields":[{"Name":"URL","Docs":"","Typewords":["string"]},{"Name":"Authorization","Docs":"","Typewords":["string"]},{"Name":"Events","Docs":"","Typewords":["[]","string"]}]},
|
||||
"IncomingWebhook": {"Name":"IncomingWebhook","Docs":"","Fields":[{"Name":"URL","Docs":"","Typewords":["string"]},{"Name":"Authorization","Docs":"","Typewords":["string"]}]},
|
||||
"SubjectPass": {"Name":"SubjectPass","Docs":"","Fields":[{"Name":"Period","Docs":"","Typewords":["int64"]}]},
|
||||
|
||||
Reference in New Issue
Block a user