From 08995c7806adecb6c6913cc1a049ff5c45e65755 Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Sun, 15 Oct 2023 15:05:20 +0200 Subject: [PATCH] webmail: when composing a message, show security status in a bar below addressee input field the bar is currently showing 3 properties: 1. mta-sts enforced; 2. mx lookup returned dnssec-signed response; 3. first delivery destination host has dane records the colors are: red for not-implemented, green for implemented, gray for error, nothing for unknown/irrelevant. the plan is to implement "requiretls" soon and start caching per domain whether delivery can be done with starttls and whether the domain supports requiretls. and show that in two new parts of the bar. thanks to damian poddebniak for pointing out that security indicators should always be visible, not only for positive/negative result. otherwise users won't notice their absence. --- mtastsdb/db.go | 5 +- webmail/api.go | 124 ++++++++++++++++++++++++++++++++++++++++++++ webmail/api.json | 73 ++++++++++++++++++++++++++ webmail/api.ts | 37 ++++++++++++- webmail/api_test.go | 7 +++ webmail/msg.js | 28 +++++++++- webmail/text.js | 28 +++++++++- webmail/webmail.js | 90 +++++++++++++++++++++++++++++--- webmail/webmail.ts | 68 +++++++++++++++++++++++- 9 files changed, 445 insertions(+), 15 deletions(-) diff --git a/mtastsdb/db.go b/mtastsdb/db.go index 7b5f262037..cef382854d 100644 --- a/mtastsdb/db.go +++ b/mtastsdb/db.go @@ -106,9 +106,12 @@ func Close() { } } -// Lookup looks up a policy for the domain in the database. +// lookup looks up a policy for the domain in the database. // // Only non-expired records are returned. +// +// Returns ErrNotFound if record is not present. +// Returns ErrBackoff if a recent attempt to fetch a record failed. func lookup(ctx context.Context, domain dns.Domain) (*PolicyRecord, error) { log := xlog.WithContext(ctx) db, err := database(ctx) diff --git a/webmail/api.go b/webmail/api.go index 47a8c5c533..01334593eb 100644 --- a/webmail/api.go +++ b/webmail/api.go @@ -11,12 +11,14 @@ import ( "mime" "mime/multipart" "mime/quotedprintable" + "net" "net/http" "net/mail" "net/textproto" "os" "sort" "strings" + "sync" "time" _ "embed" @@ -35,8 +37,11 @@ import ( "github.com/mjl-/mox/mox-" "github.com/mjl-/mox/moxio" "github.com/mjl-/mox/moxvar" + "github.com/mjl-/mox/mtasts" + "github.com/mjl-/mox/mtastsdb" "github.com/mjl-/mox/queue" "github.com/mjl-/mox/smtp" + "github.com/mjl-/mox/smtpclient" "github.com/mjl-/mox/store" ) @@ -1617,6 +1622,125 @@ func (Webmail) ThreadMute(ctx context.Context, messageIDs []int64, mute bool) { }) } +// SecurityResult indicates whether a security feature is supported. +type SecurityResult string + +const ( + SecurityResultError SecurityResult = "error" + SecurityResultNo SecurityResult = "no" + SecurityResultYes SecurityResult = "yes" + // Unknown whether supported. Finding out may only be (reasonably) possible when + // trying (e.g. SMTP STARTTLS). Once tried, the result may be cached for future + // lookups. + SecurityResultUnknown SecurityResult = "unknown" +) + +// RecipientSecurity is a quick analysis of the security properties of delivery to the recipient (domain). +// Fields are nil when an error occurred during analysis. +type RecipientSecurity struct { + MTASTS SecurityResult // Whether we have a stored enforced MTA-STS policy, or domain has MTA-STS DNS record. + DNSSEC SecurityResult // Whether MX lookup response was DNSSEC-signed. + DANE SecurityResult // Whether first delivery destination has DANE records. +} + +// RecipientSecurity looks up security properties of the address in the +// single-address message addressee (as it appears in a To/Cc/Bcc/etc header). +func (Webmail) RecipientSecurity(ctx context.Context, messageAddressee string) (RecipientSecurity, error) { + resolver := dns.StrictResolver{Pkg: "webmail"} + return recipientSecurity(ctx, resolver, messageAddressee) +} + +// separate function for testing with mocked resolver. +func recipientSecurity(ctx context.Context, resolver dns.Resolver, messageAddressee string) (RecipientSecurity, error) { + log := xlog.WithContext(ctx) + + rs := RecipientSecurity{ + SecurityResultUnknown, + SecurityResultUnknown, + SecurityResultUnknown, + } + + msgAddr, err := mail.ParseAddress(messageAddressee) + if err != nil { + return rs, fmt.Errorf("parsing message addressee: %v", err) + } + + addr, err := smtp.ParseAddress(msgAddr.Address) + if err != nil { + return rs, fmt.Errorf("parsing address: %v", err) + } + + var wg sync.WaitGroup + + // MTA-STS. + wg.Add(1) + go func() { + defer wg.Done() + + policy, _, err := mtastsdb.Get(ctx, resolver, addr.Domain) + if policy != nil && policy.Mode == mtasts.ModeEnforce { + rs.MTASTS = SecurityResultYes + } else if err == nil { + rs.MTASTS = SecurityResultNo + } else { + rs.MTASTS = SecurityResultError + } + }() + + // DNSSEC and DANE. + wg.Add(1) + go func() { + defer wg.Done() + + _, origNextHopAuthentic, expandedNextHopAuthentic, _, hosts, _, err := smtpclient.GatherDestinations(ctx, log, resolver, dns.IPDomain{Domain: addr.Domain}) + if err != nil { + rs.DNSSEC = SecurityResultError + return + } + if origNextHopAuthentic && expandedNextHopAuthentic { + rs.DNSSEC = SecurityResultYes + } else { + rs.DNSSEC = SecurityResultNo + } + + if !origNextHopAuthentic { + rs.DANE = SecurityResultNo + return + } + + // We're only looking at the first host to deliver to (typically first mx destination). + if len(hosts) == 0 || hosts[0].Domain.IsZero() { + return // Should not happen. + } + host := hosts[0] + + // Resolve the IPs. Required for DANE to prevent bad DNS servers from causing an + // error result instead of no-DANE result. + authentic, expandedAuthentic, expandedHost, _, _, err := smtpclient.GatherIPs(ctx, log, resolver, host, map[string][]net.IP{}) + if err != nil { + rs.DANE = SecurityResultError + return + } + if !authentic { + rs.DANE = SecurityResultNo + return + } + + daneRequired, _, _, err := smtpclient.GatherTLSA(ctx, log, resolver, host.Domain, expandedAuthentic, expandedHost) + if err != nil { + rs.DANE = SecurityResultError + return + } else if daneRequired { + rs.DANE = SecurityResultYes + } else { + rs.DANE = SecurityResultNo + } + }() + + wg.Wait() + return rs, nil +} + func slicesAny[T any](l []T) []any { r := make([]any, len(l)) for i, v := range l { diff --git a/webmail/api.json b/webmail/api.json index 7663876fd1..7598ca5bad 100644 --- a/webmail/api.json +++ b/webmail/api.json @@ -275,6 +275,26 @@ ], "Returns": [] }, + { + "Name": "RecipientSecurity", + "Docs": "RecipientSecurity looks up security properties of the address in the\nsingle-address message addressee (as it appears in a To/Cc/Bcc/etc header).", + "Params": [ + { + "Name": "messageAddressee", + "Typewords": [ + "string" + ] + } + ], + "Returns": [ + { + "Name": "r0", + "Typewords": [ + "RecipientSecurity" + ] + } + ] + }, { "Name": "SSETypes", "Docs": "SSETypes exists to ensure the generated API contains the types, for use in SSE events.", @@ -1234,6 +1254,33 @@ } ] }, + { + "Name": "RecipientSecurity", + "Docs": "RecipientSecurity is a quick analysis of the security properties of delivery to the recipient (domain).\nFields are nil when an error occurred during analysis.", + "Fields": [ + { + "Name": "MTASTS", + "Docs": "Whether we have a stored enforced MTA-STS policy, or domain has MTA-STS DNS record.", + "Typewords": [ + "SecurityResult" + ] + }, + { + "Name": "DNSSEC", + "Docs": "Whether MX lookup response was DNSSEC-signed.", + "Typewords": [ + "SecurityResult" + ] + }, + { + "Name": "DANE", + "Docs": "Whether first delivery destination has DANE records.", + "Typewords": [ + "SecurityResult" + ] + } + ] + }, { "Name": "EventStart", "Docs": "EventStart is the first message sent on an SSE connection, giving the client\nbasic data to populate its UI. After this event, messages will follow quickly in\nan EventViewMsgs event.", @@ -2587,6 +2634,32 @@ } ] }, + { + "Name": "SecurityResult", + "Docs": "SecurityResult indicates whether a security feature is supported.", + "Values": [ + { + "Name": "SecurityResultError", + "Value": "error", + "Docs": "" + }, + { + "Name": "SecurityResultNo", + "Value": "no", + "Docs": "" + }, + { + "Name": "SecurityResultYes", + "Value": "yes", + "Docs": "" + }, + { + "Name": "SecurityResultUnknown", + "Value": "unknown", + "Docs": "Unknown whether supported. Finding out may only be (reasonably) possible when\ntrying (e.g. SMTP STARTTLS). Once tried, the result may be cached for future\nlookups." + } + ] + }, { "Name": "Localpart", "Docs": "Localpart is a decoded local part of an email address, before the \"@\".\nFor quoted strings, values do not hold the double quote or escaping backslashes.\nAn empty string can be a valid localpart.", diff --git a/webmail/api.ts b/webmail/api.ts index 6c808367a9..19e089eb21 100644 --- a/webmail/api.ts +++ b/webmail/api.ts @@ -177,6 +177,14 @@ export interface Mailbox { Size: number // Number of bytes for all messages. } +// RecipientSecurity is a quick analysis of the security properties of delivery to the recipient (domain). +// Fields are nil when an error occurred during analysis. +export interface RecipientSecurity { + MTASTS: SecurityResult // Whether we have a stored enforced MTA-STS policy, or domain has MTA-STS DNS record. + DNSSEC: SecurityResult // Whether MX lookup response was DNSSEC-signed. + DANE: SecurityResult // Whether first delivery destination has DANE records. +} + // EventStart is the first message sent on an SSE connection, giving the client // basic data to populate its UI. After this event, messages will follow quickly in // an EventViewMsgs event. @@ -480,13 +488,24 @@ export enum AttachmentType { AttachmentPresentation = "presentation", // odp, pptx, ... } +// SecurityResult indicates whether a security feature is supported. +export enum SecurityResult { + SecurityResultError = "error", + SecurityResultNo = "no", + SecurityResultYes = "yes", + // Unknown whether supported. Finding out may only be (reasonably) possible when + // trying (e.g. SMTP STARTTLS). Once tried, the result may be cached for future + // lookups. + SecurityResultUnknown = "unknown", +} + // Localpart is a decoded local part of an email address, before the "@". // For quoted strings, values do not hold the double quote or escaping backslashes. // An empty string can be a valid localpart. export type Localpart = string -export const structTypes: {[typename: string]: boolean} = {"Address":true,"Attachment":true,"ChangeMailboxAdd":true,"ChangeMailboxCounts":true,"ChangeMailboxKeywords":true,"ChangeMailboxRemove":true,"ChangeMailboxRename":true,"ChangeMailboxSpecialUse":true,"ChangeMsgAdd":true,"ChangeMsgFlags":true,"ChangeMsgRemove":true,"ChangeMsgThread":true,"Domain":true,"DomainAddressConfig":true,"Envelope":true,"EventStart":true,"EventViewChanges":true,"EventViewErr":true,"EventViewMsgs":true,"EventViewReset":true,"File":true,"Filter":true,"Flags":true,"ForwardAttachments":true,"Mailbox":true,"Message":true,"MessageAddress":true,"MessageEnvelope":true,"MessageItem":true,"NotFilter":true,"Page":true,"ParsedMessage":true,"Part":true,"Query":true,"Request":true,"SpecialUse":true,"SubmitMessage":true} -export const stringsTypes: {[typename: string]: boolean} = {"AttachmentType":true,"Localpart":true,"ThreadMode":true} +export const structTypes: {[typename: string]: boolean} = {"Address":true,"Attachment":true,"ChangeMailboxAdd":true,"ChangeMailboxCounts":true,"ChangeMailboxKeywords":true,"ChangeMailboxRemove":true,"ChangeMailboxRename":true,"ChangeMailboxSpecialUse":true,"ChangeMsgAdd":true,"ChangeMsgFlags":true,"ChangeMsgRemove":true,"ChangeMsgThread":true,"Domain":true,"DomainAddressConfig":true,"Envelope":true,"EventStart":true,"EventViewChanges":true,"EventViewErr":true,"EventViewMsgs":true,"EventViewReset":true,"File":true,"Filter":true,"Flags":true,"ForwardAttachments":true,"Mailbox":true,"Message":true,"MessageAddress":true,"MessageEnvelope":true,"MessageItem":true,"NotFilter":true,"Page":true,"ParsedMessage":true,"Part":true,"Query":true,"RecipientSecurity":true,"Request":true,"SpecialUse":true,"SubmitMessage":true} +export const stringsTypes: {[typename: string]: boolean} = {"AttachmentType":true,"Localpart":true,"SecurityResult":true,"ThreadMode":true} export const intsTypes: {[typename: string]: boolean} = {"ModSeq":true,"UID":true,"Validation":true} export const types: TypenameMap = { "Request": {"Name":"Request","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"SSEID","Docs":"","Typewords":["int64"]},{"Name":"ViewID","Docs":"","Typewords":["int64"]},{"Name":"Cancel","Docs":"","Typewords":["bool"]},{"Name":"Query","Docs":"","Typewords":["Query"]},{"Name":"Page","Docs":"","Typewords":["Page"]}]}, @@ -504,6 +523,7 @@ export const types: TypenameMap = { "File": {"Name":"File","Docs":"","Fields":[{"Name":"Filename","Docs":"","Typewords":["string"]},{"Name":"DataURI","Docs":"","Typewords":["string"]}]}, "ForwardAttachments": {"Name":"ForwardAttachments","Docs":"","Fields":[{"Name":"MessageID","Docs":"","Typewords":["int64"]},{"Name":"Paths","Docs":"","Typewords":["[]","[]","int32"]}]}, "Mailbox": {"Name":"Mailbox","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"UIDValidity","Docs":"","Typewords":["uint32"]},{"Name":"UIDNext","Docs":"","Typewords":["UID"]},{"Name":"Archive","Docs":"","Typewords":["bool"]},{"Name":"Draft","Docs":"","Typewords":["bool"]},{"Name":"Junk","Docs":"","Typewords":["bool"]},{"Name":"Sent","Docs":"","Typewords":["bool"]},{"Name":"Trash","Docs":"","Typewords":["bool"]},{"Name":"Keywords","Docs":"","Typewords":["[]","string"]},{"Name":"HaveCounts","Docs":"","Typewords":["bool"]},{"Name":"Total","Docs":"","Typewords":["int64"]},{"Name":"Deleted","Docs":"","Typewords":["int64"]},{"Name":"Unread","Docs":"","Typewords":["int64"]},{"Name":"Unseen","Docs":"","Typewords":["int64"]},{"Name":"Size","Docs":"","Typewords":["int64"]}]}, + "RecipientSecurity": {"Name":"RecipientSecurity","Docs":"","Fields":[{"Name":"MTASTS","Docs":"","Typewords":["SecurityResult"]},{"Name":"DNSSEC","Docs":"","Typewords":["SecurityResult"]},{"Name":"DANE","Docs":"","Typewords":["SecurityResult"]}]}, "EventStart": {"Name":"EventStart","Docs":"","Fields":[{"Name":"SSEID","Docs":"","Typewords":["int64"]},{"Name":"LoginAddress","Docs":"","Typewords":["MessageAddress"]},{"Name":"Addresses","Docs":"","Typewords":["[]","MessageAddress"]},{"Name":"DomainAddressConfigs","Docs":"","Typewords":["{}","DomainAddressConfig"]},{"Name":"MailboxName","Docs":"","Typewords":["string"]},{"Name":"Mailboxes","Docs":"","Typewords":["[]","Mailbox"]}]}, "DomainAddressConfig": {"Name":"DomainAddressConfig","Docs":"","Fields":[{"Name":"LocalpartCatchallSeparator","Docs":"","Typewords":["string"]},{"Name":"LocalpartCaseSensitive","Docs":"","Typewords":["bool"]}]}, "EventViewErr": {"Name":"EventViewErr","Docs":"","Fields":[{"Name":"ViewID","Docs":"","Typewords":["int64"]},{"Name":"RequestID","Docs":"","Typewords":["int64"]},{"Name":"Err","Docs":"","Typewords":["string"]}]}, @@ -531,6 +551,7 @@ export const types: TypenameMap = { "Validation": {"Name":"Validation","Docs":"","Values":[{"Name":"ValidationUnknown","Value":0,"Docs":""},{"Name":"ValidationStrict","Value":1,"Docs":""},{"Name":"ValidationDMARC","Value":2,"Docs":""},{"Name":"ValidationRelaxed","Value":3,"Docs":""},{"Name":"ValidationPass","Value":4,"Docs":""},{"Name":"ValidationNeutral","Value":5,"Docs":""},{"Name":"ValidationTemperror","Value":6,"Docs":""},{"Name":"ValidationPermerror","Value":7,"Docs":""},{"Name":"ValidationFail","Value":8,"Docs":""},{"Name":"ValidationSoftfail","Value":9,"Docs":""},{"Name":"ValidationNone","Value":10,"Docs":""}]}, "ThreadMode": {"Name":"ThreadMode","Docs":"","Values":[{"Name":"ThreadOff","Value":"off","Docs":""},{"Name":"ThreadOn","Value":"on","Docs":""},{"Name":"ThreadUnread","Value":"unread","Docs":""}]}, "AttachmentType": {"Name":"AttachmentType","Docs":"","Values":[{"Name":"AttachmentIndifferent","Value":"","Docs":""},{"Name":"AttachmentNone","Value":"none","Docs":""},{"Name":"AttachmentAny","Value":"any","Docs":""},{"Name":"AttachmentImage","Value":"image","Docs":""},{"Name":"AttachmentPDF","Value":"pdf","Docs":""},{"Name":"AttachmentArchive","Value":"archive","Docs":""},{"Name":"AttachmentSpreadsheet","Value":"spreadsheet","Docs":""},{"Name":"AttachmentDocument","Value":"document","Docs":""},{"Name":"AttachmentPresentation","Value":"presentation","Docs":""}]}, + "SecurityResult": {"Name":"SecurityResult","Docs":"","Values":[{"Name":"SecurityResultError","Value":"error","Docs":""},{"Name":"SecurityResultNo","Value":"no","Docs":""},{"Name":"SecurityResultYes","Value":"yes","Docs":""},{"Name":"SecurityResultUnknown","Value":"unknown","Docs":""}]}, "Localpart": {"Name":"Localpart","Docs":"","Values":null}, } @@ -550,6 +571,7 @@ export const parser = { File: (v: any) => parse("File", v) as File, ForwardAttachments: (v: any) => parse("ForwardAttachments", v) as ForwardAttachments, Mailbox: (v: any) => parse("Mailbox", v) as Mailbox, + RecipientSecurity: (v: any) => parse("RecipientSecurity", v) as RecipientSecurity, EventStart: (v: any) => parse("EventStart", v) as EventStart, DomainAddressConfig: (v: any) => parse("DomainAddressConfig", v) as DomainAddressConfig, EventViewErr: (v: any) => parse("EventViewErr", v) as EventViewErr, @@ -577,6 +599,7 @@ export const parser = { Validation: (v: any) => parse("Validation", v) as Validation, ThreadMode: (v: any) => parse("ThreadMode", v) as ThreadMode, AttachmentType: (v: any) => parse("AttachmentType", v) as AttachmentType, + SecurityResult: (v: any) => parse("SecurityResult", v) as SecurityResult, Localpart: (v: any) => parse("Localpart", v) as Localpart, } @@ -758,6 +781,16 @@ export class Client { return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params) as void } + // RecipientSecurity looks up security properties of the address in the + // single-address message addressee (as it appears in a To/Cc/Bcc/etc header). + async RecipientSecurity(messageAddressee: string): Promise { + const fn: string = "RecipientSecurity" + const paramTypes: string[][] = [["string"]] + const returnTypes: string[][] = [["RecipientSecurity"]] + const params: any[] = [messageAddressee] + return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params) as RecipientSecurity + } + // SSETypes exists to ensure the generated API contains the types, for use in SSE events. async SSETypes(): Promise<[EventStart, EventViewErr, EventViewReset, EventViewMsgs, EventViewChanges, ChangeMsgAdd, ChangeMsgRemove, ChangeMsgFlags, ChangeMsgThread, ChangeMailboxRemove, ChangeMailboxAdd, ChangeMailboxRename, ChangeMailboxCounts, ChangeMailboxSpecialUse, ChangeMailboxKeywords, Flags]> { const fn: string = "SSETypes" diff --git a/webmail/api_test.go b/webmail/api_test.go index d6a98e483c..dde7a115f7 100644 --- a/webmail/api_test.go +++ b/webmail/api_test.go @@ -12,6 +12,7 @@ import ( "github.com/mjl-/bstore" "github.com/mjl-/sherpa" + "github.com/mjl-/mox/dns" "github.com/mjl-/mox/mox-" "github.com/mjl-/mox/queue" "github.com/mjl-/mox/store" @@ -362,4 +363,10 @@ func TestAPI(t *testing.T) { l, full = api.CompleteRecipient(ctx, "cc2") tcompare(t, l, []string{"mjl cc2 "}) tcompare(t, full, true) + + // RecipientSecurity + resolver := dns.MockResolver{} + rs, err := recipientSecurity(ctxbg, resolver, "mjl@a.mox.example") + tcompare(t, err, nil) + tcompare(t, rs, RecipientSecurity{SecurityResultNo, SecurityResultNo, SecurityResultNo}) } diff --git a/webmail/msg.js b/webmail/msg.js index 2e406860d5..748b03c722 100644 --- a/webmail/msg.js +++ b/webmail/msg.js @@ -36,8 +36,19 @@ var api; AttachmentType["AttachmentDocument"] = "document"; AttachmentType["AttachmentPresentation"] = "presentation"; })(AttachmentType = api.AttachmentType || (api.AttachmentType = {})); - api.structTypes = { "Address": true, "Attachment": true, "ChangeMailboxAdd": true, "ChangeMailboxCounts": true, "ChangeMailboxKeywords": true, "ChangeMailboxRemove": true, "ChangeMailboxRename": true, "ChangeMailboxSpecialUse": true, "ChangeMsgAdd": true, "ChangeMsgFlags": true, "ChangeMsgRemove": true, "ChangeMsgThread": true, "Domain": true, "DomainAddressConfig": true, "Envelope": true, "EventStart": true, "EventViewChanges": true, "EventViewErr": true, "EventViewMsgs": true, "EventViewReset": true, "File": true, "Filter": true, "Flags": true, "ForwardAttachments": true, "Mailbox": true, "Message": true, "MessageAddress": true, "MessageEnvelope": true, "MessageItem": true, "NotFilter": true, "Page": true, "ParsedMessage": true, "Part": true, "Query": true, "Request": true, "SpecialUse": true, "SubmitMessage": true }; - api.stringsTypes = { "AttachmentType": true, "Localpart": true, "ThreadMode": true }; + // SecurityResult indicates whether a security feature is supported. + let SecurityResult; + (function (SecurityResult) { + SecurityResult["SecurityResultError"] = "error"; + SecurityResult["SecurityResultNo"] = "no"; + SecurityResult["SecurityResultYes"] = "yes"; + // Unknown whether supported. Finding out may only be (reasonably) possible when + // trying (e.g. SMTP STARTTLS). Once tried, the result may be cached for future + // lookups. + SecurityResult["SecurityResultUnknown"] = "unknown"; + })(SecurityResult = api.SecurityResult || (api.SecurityResult = {})); + api.structTypes = { "Address": true, "Attachment": true, "ChangeMailboxAdd": true, "ChangeMailboxCounts": true, "ChangeMailboxKeywords": true, "ChangeMailboxRemove": true, "ChangeMailboxRename": true, "ChangeMailboxSpecialUse": true, "ChangeMsgAdd": true, "ChangeMsgFlags": true, "ChangeMsgRemove": true, "ChangeMsgThread": true, "Domain": true, "DomainAddressConfig": true, "Envelope": true, "EventStart": true, "EventViewChanges": true, "EventViewErr": true, "EventViewMsgs": true, "EventViewReset": true, "File": true, "Filter": true, "Flags": true, "ForwardAttachments": true, "Mailbox": true, "Message": true, "MessageAddress": true, "MessageEnvelope": true, "MessageItem": true, "NotFilter": true, "Page": true, "ParsedMessage": true, "Part": true, "Query": true, "RecipientSecurity": true, "Request": true, "SpecialUse": true, "SubmitMessage": true }; + api.stringsTypes = { "AttachmentType": true, "Localpart": true, "SecurityResult": true, "ThreadMode": true }; api.intsTypes = { "ModSeq": true, "UID": true, "Validation": true }; api.types = { "Request": { "Name": "Request", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Cancel", "Docs": "", "Typewords": ["bool"] }, { "Name": "Query", "Docs": "", "Typewords": ["Query"] }, { "Name": "Page", "Docs": "", "Typewords": ["Page"] }] }, @@ -55,6 +66,7 @@ var api; "File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] }, "ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] }, "Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] }, + "RecipientSecurity": { "Name": "RecipientSecurity", "Docs": "", "Fields": [{ "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }] }, "EventStart": { "Name": "EventStart", "Docs": "", "Fields": [{ "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginAddress", "Docs": "", "Typewords": ["MessageAddress"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "DomainAddressConfigs", "Docs": "", "Typewords": ["{}", "DomainAddressConfig"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailboxes", "Docs": "", "Typewords": ["[]", "Mailbox"] }] }, "DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] }, "EventViewErr": { "Name": "EventViewErr", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Err", "Docs": "", "Typewords": ["string"] }] }, @@ -82,6 +94,7 @@ var api; "Validation": { "Name": "Validation", "Docs": "", "Values": [{ "Name": "ValidationUnknown", "Value": 0, "Docs": "" }, { "Name": "ValidationStrict", "Value": 1, "Docs": "" }, { "Name": "ValidationDMARC", "Value": 2, "Docs": "" }, { "Name": "ValidationRelaxed", "Value": 3, "Docs": "" }, { "Name": "ValidationPass", "Value": 4, "Docs": "" }, { "Name": "ValidationNeutral", "Value": 5, "Docs": "" }, { "Name": "ValidationTemperror", "Value": 6, "Docs": "" }, { "Name": "ValidationPermerror", "Value": 7, "Docs": "" }, { "Name": "ValidationFail", "Value": 8, "Docs": "" }, { "Name": "ValidationSoftfail", "Value": 9, "Docs": "" }, { "Name": "ValidationNone", "Value": 10, "Docs": "" }] }, "ThreadMode": { "Name": "ThreadMode", "Docs": "", "Values": [{ "Name": "ThreadOff", "Value": "off", "Docs": "" }, { "Name": "ThreadOn", "Value": "on", "Docs": "" }, { "Name": "ThreadUnread", "Value": "unread", "Docs": "" }] }, "AttachmentType": { "Name": "AttachmentType", "Docs": "", "Values": [{ "Name": "AttachmentIndifferent", "Value": "", "Docs": "" }, { "Name": "AttachmentNone", "Value": "none", "Docs": "" }, { "Name": "AttachmentAny", "Value": "any", "Docs": "" }, { "Name": "AttachmentImage", "Value": "image", "Docs": "" }, { "Name": "AttachmentPDF", "Value": "pdf", "Docs": "" }, { "Name": "AttachmentArchive", "Value": "archive", "Docs": "" }, { "Name": "AttachmentSpreadsheet", "Value": "spreadsheet", "Docs": "" }, { "Name": "AttachmentDocument", "Value": "document", "Docs": "" }, { "Name": "AttachmentPresentation", "Value": "presentation", "Docs": "" }] }, + "SecurityResult": { "Name": "SecurityResult", "Docs": "", "Values": [{ "Name": "SecurityResultError", "Value": "error", "Docs": "" }, { "Name": "SecurityResultNo", "Value": "no", "Docs": "" }, { "Name": "SecurityResultYes", "Value": "yes", "Docs": "" }, { "Name": "SecurityResultUnknown", "Value": "unknown", "Docs": "" }] }, "Localpart": { "Name": "Localpart", "Docs": "", "Values": null }, }; api.parser = { @@ -100,6 +113,7 @@ var api; File: (v) => api.parse("File", v), ForwardAttachments: (v) => api.parse("ForwardAttachments", v), Mailbox: (v) => api.parse("Mailbox", v), + RecipientSecurity: (v) => api.parse("RecipientSecurity", v), EventStart: (v) => api.parse("EventStart", v), DomainAddressConfig: (v) => api.parse("DomainAddressConfig", v), EventViewErr: (v) => api.parse("EventViewErr", v), @@ -127,6 +141,7 @@ var api; Validation: (v) => api.parse("Validation", v), ThreadMode: (v) => api.parse("ThreadMode", v), AttachmentType: (v) => api.parse("AttachmentType", v), + SecurityResult: (v) => api.parse("SecurityResult", v), Localpart: (v) => api.parse("Localpart", v), }; let defaultOptions = { slicesNullable: true, mapsNullable: true, nullableOptional: true }; @@ -290,6 +305,15 @@ var api; const params = [messageIDs, mute]; return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params); } + // RecipientSecurity looks up security properties of the address in the + // single-address message addressee (as it appears in a To/Cc/Bcc/etc header). + async RecipientSecurity(messageAddressee) { + const fn = "RecipientSecurity"; + const paramTypes = [["string"]]; + const returnTypes = [["RecipientSecurity"]]; + const params = [messageAddressee]; + return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params); + } // SSETypes exists to ensure the generated API contains the types, for use in SSE events. async SSETypes() { const fn = "SSETypes"; diff --git a/webmail/text.js b/webmail/text.js index e82608c6ed..cdfaea8eb9 100644 --- a/webmail/text.js +++ b/webmail/text.js @@ -36,8 +36,19 @@ var api; AttachmentType["AttachmentDocument"] = "document"; AttachmentType["AttachmentPresentation"] = "presentation"; })(AttachmentType = api.AttachmentType || (api.AttachmentType = {})); - api.structTypes = { "Address": true, "Attachment": true, "ChangeMailboxAdd": true, "ChangeMailboxCounts": true, "ChangeMailboxKeywords": true, "ChangeMailboxRemove": true, "ChangeMailboxRename": true, "ChangeMailboxSpecialUse": true, "ChangeMsgAdd": true, "ChangeMsgFlags": true, "ChangeMsgRemove": true, "ChangeMsgThread": true, "Domain": true, "DomainAddressConfig": true, "Envelope": true, "EventStart": true, "EventViewChanges": true, "EventViewErr": true, "EventViewMsgs": true, "EventViewReset": true, "File": true, "Filter": true, "Flags": true, "ForwardAttachments": true, "Mailbox": true, "Message": true, "MessageAddress": true, "MessageEnvelope": true, "MessageItem": true, "NotFilter": true, "Page": true, "ParsedMessage": true, "Part": true, "Query": true, "Request": true, "SpecialUse": true, "SubmitMessage": true }; - api.stringsTypes = { "AttachmentType": true, "Localpart": true, "ThreadMode": true }; + // SecurityResult indicates whether a security feature is supported. + let SecurityResult; + (function (SecurityResult) { + SecurityResult["SecurityResultError"] = "error"; + SecurityResult["SecurityResultNo"] = "no"; + SecurityResult["SecurityResultYes"] = "yes"; + // Unknown whether supported. Finding out may only be (reasonably) possible when + // trying (e.g. SMTP STARTTLS). Once tried, the result may be cached for future + // lookups. + SecurityResult["SecurityResultUnknown"] = "unknown"; + })(SecurityResult = api.SecurityResult || (api.SecurityResult = {})); + api.structTypes = { "Address": true, "Attachment": true, "ChangeMailboxAdd": true, "ChangeMailboxCounts": true, "ChangeMailboxKeywords": true, "ChangeMailboxRemove": true, "ChangeMailboxRename": true, "ChangeMailboxSpecialUse": true, "ChangeMsgAdd": true, "ChangeMsgFlags": true, "ChangeMsgRemove": true, "ChangeMsgThread": true, "Domain": true, "DomainAddressConfig": true, "Envelope": true, "EventStart": true, "EventViewChanges": true, "EventViewErr": true, "EventViewMsgs": true, "EventViewReset": true, "File": true, "Filter": true, "Flags": true, "ForwardAttachments": true, "Mailbox": true, "Message": true, "MessageAddress": true, "MessageEnvelope": true, "MessageItem": true, "NotFilter": true, "Page": true, "ParsedMessage": true, "Part": true, "Query": true, "RecipientSecurity": true, "Request": true, "SpecialUse": true, "SubmitMessage": true }; + api.stringsTypes = { "AttachmentType": true, "Localpart": true, "SecurityResult": true, "ThreadMode": true }; api.intsTypes = { "ModSeq": true, "UID": true, "Validation": true }; api.types = { "Request": { "Name": "Request", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Cancel", "Docs": "", "Typewords": ["bool"] }, { "Name": "Query", "Docs": "", "Typewords": ["Query"] }, { "Name": "Page", "Docs": "", "Typewords": ["Page"] }] }, @@ -55,6 +66,7 @@ var api; "File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] }, "ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] }, "Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] }, + "RecipientSecurity": { "Name": "RecipientSecurity", "Docs": "", "Fields": [{ "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }] }, "EventStart": { "Name": "EventStart", "Docs": "", "Fields": [{ "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginAddress", "Docs": "", "Typewords": ["MessageAddress"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "DomainAddressConfigs", "Docs": "", "Typewords": ["{}", "DomainAddressConfig"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailboxes", "Docs": "", "Typewords": ["[]", "Mailbox"] }] }, "DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] }, "EventViewErr": { "Name": "EventViewErr", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Err", "Docs": "", "Typewords": ["string"] }] }, @@ -82,6 +94,7 @@ var api; "Validation": { "Name": "Validation", "Docs": "", "Values": [{ "Name": "ValidationUnknown", "Value": 0, "Docs": "" }, { "Name": "ValidationStrict", "Value": 1, "Docs": "" }, { "Name": "ValidationDMARC", "Value": 2, "Docs": "" }, { "Name": "ValidationRelaxed", "Value": 3, "Docs": "" }, { "Name": "ValidationPass", "Value": 4, "Docs": "" }, { "Name": "ValidationNeutral", "Value": 5, "Docs": "" }, { "Name": "ValidationTemperror", "Value": 6, "Docs": "" }, { "Name": "ValidationPermerror", "Value": 7, "Docs": "" }, { "Name": "ValidationFail", "Value": 8, "Docs": "" }, { "Name": "ValidationSoftfail", "Value": 9, "Docs": "" }, { "Name": "ValidationNone", "Value": 10, "Docs": "" }] }, "ThreadMode": { "Name": "ThreadMode", "Docs": "", "Values": [{ "Name": "ThreadOff", "Value": "off", "Docs": "" }, { "Name": "ThreadOn", "Value": "on", "Docs": "" }, { "Name": "ThreadUnread", "Value": "unread", "Docs": "" }] }, "AttachmentType": { "Name": "AttachmentType", "Docs": "", "Values": [{ "Name": "AttachmentIndifferent", "Value": "", "Docs": "" }, { "Name": "AttachmentNone", "Value": "none", "Docs": "" }, { "Name": "AttachmentAny", "Value": "any", "Docs": "" }, { "Name": "AttachmentImage", "Value": "image", "Docs": "" }, { "Name": "AttachmentPDF", "Value": "pdf", "Docs": "" }, { "Name": "AttachmentArchive", "Value": "archive", "Docs": "" }, { "Name": "AttachmentSpreadsheet", "Value": "spreadsheet", "Docs": "" }, { "Name": "AttachmentDocument", "Value": "document", "Docs": "" }, { "Name": "AttachmentPresentation", "Value": "presentation", "Docs": "" }] }, + "SecurityResult": { "Name": "SecurityResult", "Docs": "", "Values": [{ "Name": "SecurityResultError", "Value": "error", "Docs": "" }, { "Name": "SecurityResultNo", "Value": "no", "Docs": "" }, { "Name": "SecurityResultYes", "Value": "yes", "Docs": "" }, { "Name": "SecurityResultUnknown", "Value": "unknown", "Docs": "" }] }, "Localpart": { "Name": "Localpart", "Docs": "", "Values": null }, }; api.parser = { @@ -100,6 +113,7 @@ var api; File: (v) => api.parse("File", v), ForwardAttachments: (v) => api.parse("ForwardAttachments", v), Mailbox: (v) => api.parse("Mailbox", v), + RecipientSecurity: (v) => api.parse("RecipientSecurity", v), EventStart: (v) => api.parse("EventStart", v), DomainAddressConfig: (v) => api.parse("DomainAddressConfig", v), EventViewErr: (v) => api.parse("EventViewErr", v), @@ -127,6 +141,7 @@ var api; Validation: (v) => api.parse("Validation", v), ThreadMode: (v) => api.parse("ThreadMode", v), AttachmentType: (v) => api.parse("AttachmentType", v), + SecurityResult: (v) => api.parse("SecurityResult", v), Localpart: (v) => api.parse("Localpart", v), }; let defaultOptions = { slicesNullable: true, mapsNullable: true, nullableOptional: true }; @@ -290,6 +305,15 @@ var api; const params = [messageIDs, mute]; return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params); } + // RecipientSecurity looks up security properties of the address in the + // single-address message addressee (as it appears in a To/Cc/Bcc/etc header). + async RecipientSecurity(messageAddressee) { + const fn = "RecipientSecurity"; + const paramTypes = [["string"]]; + const returnTypes = [["RecipientSecurity"]]; + const params = [messageAddressee]; + return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params); + } // SSETypes exists to ensure the generated API contains the types, for use in SSE events. async SSETypes() { const fn = "SSETypes"; diff --git a/webmail/webmail.js b/webmail/webmail.js index a2d4d65cda..75176289dd 100644 --- a/webmail/webmail.js +++ b/webmail/webmail.js @@ -36,8 +36,19 @@ var api; AttachmentType["AttachmentDocument"] = "document"; AttachmentType["AttachmentPresentation"] = "presentation"; })(AttachmentType = api.AttachmentType || (api.AttachmentType = {})); - api.structTypes = { "Address": true, "Attachment": true, "ChangeMailboxAdd": true, "ChangeMailboxCounts": true, "ChangeMailboxKeywords": true, "ChangeMailboxRemove": true, "ChangeMailboxRename": true, "ChangeMailboxSpecialUse": true, "ChangeMsgAdd": true, "ChangeMsgFlags": true, "ChangeMsgRemove": true, "ChangeMsgThread": true, "Domain": true, "DomainAddressConfig": true, "Envelope": true, "EventStart": true, "EventViewChanges": true, "EventViewErr": true, "EventViewMsgs": true, "EventViewReset": true, "File": true, "Filter": true, "Flags": true, "ForwardAttachments": true, "Mailbox": true, "Message": true, "MessageAddress": true, "MessageEnvelope": true, "MessageItem": true, "NotFilter": true, "Page": true, "ParsedMessage": true, "Part": true, "Query": true, "Request": true, "SpecialUse": true, "SubmitMessage": true }; - api.stringsTypes = { "AttachmentType": true, "Localpart": true, "ThreadMode": true }; + // SecurityResult indicates whether a security feature is supported. + let SecurityResult; + (function (SecurityResult) { + SecurityResult["SecurityResultError"] = "error"; + SecurityResult["SecurityResultNo"] = "no"; + SecurityResult["SecurityResultYes"] = "yes"; + // Unknown whether supported. Finding out may only be (reasonably) possible when + // trying (e.g. SMTP STARTTLS). Once tried, the result may be cached for future + // lookups. + SecurityResult["SecurityResultUnknown"] = "unknown"; + })(SecurityResult = api.SecurityResult || (api.SecurityResult = {})); + api.structTypes = { "Address": true, "Attachment": true, "ChangeMailboxAdd": true, "ChangeMailboxCounts": true, "ChangeMailboxKeywords": true, "ChangeMailboxRemove": true, "ChangeMailboxRename": true, "ChangeMailboxSpecialUse": true, "ChangeMsgAdd": true, "ChangeMsgFlags": true, "ChangeMsgRemove": true, "ChangeMsgThread": true, "Domain": true, "DomainAddressConfig": true, "Envelope": true, "EventStart": true, "EventViewChanges": true, "EventViewErr": true, "EventViewMsgs": true, "EventViewReset": true, "File": true, "Filter": true, "Flags": true, "ForwardAttachments": true, "Mailbox": true, "Message": true, "MessageAddress": true, "MessageEnvelope": true, "MessageItem": true, "NotFilter": true, "Page": true, "ParsedMessage": true, "Part": true, "Query": true, "RecipientSecurity": true, "Request": true, "SpecialUse": true, "SubmitMessage": true }; + api.stringsTypes = { "AttachmentType": true, "Localpart": true, "SecurityResult": true, "ThreadMode": true }; api.intsTypes = { "ModSeq": true, "UID": true, "Validation": true }; api.types = { "Request": { "Name": "Request", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Cancel", "Docs": "", "Typewords": ["bool"] }, { "Name": "Query", "Docs": "", "Typewords": ["Query"] }, { "Name": "Page", "Docs": "", "Typewords": ["Page"] }] }, @@ -55,6 +66,7 @@ var api; "File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] }, "ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] }, "Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] }, + "RecipientSecurity": { "Name": "RecipientSecurity", "Docs": "", "Fields": [{ "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }] }, "EventStart": { "Name": "EventStart", "Docs": "", "Fields": [{ "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginAddress", "Docs": "", "Typewords": ["MessageAddress"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "DomainAddressConfigs", "Docs": "", "Typewords": ["{}", "DomainAddressConfig"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailboxes", "Docs": "", "Typewords": ["[]", "Mailbox"] }] }, "DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] }, "EventViewErr": { "Name": "EventViewErr", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Err", "Docs": "", "Typewords": ["string"] }] }, @@ -82,6 +94,7 @@ var api; "Validation": { "Name": "Validation", "Docs": "", "Values": [{ "Name": "ValidationUnknown", "Value": 0, "Docs": "" }, { "Name": "ValidationStrict", "Value": 1, "Docs": "" }, { "Name": "ValidationDMARC", "Value": 2, "Docs": "" }, { "Name": "ValidationRelaxed", "Value": 3, "Docs": "" }, { "Name": "ValidationPass", "Value": 4, "Docs": "" }, { "Name": "ValidationNeutral", "Value": 5, "Docs": "" }, { "Name": "ValidationTemperror", "Value": 6, "Docs": "" }, { "Name": "ValidationPermerror", "Value": 7, "Docs": "" }, { "Name": "ValidationFail", "Value": 8, "Docs": "" }, { "Name": "ValidationSoftfail", "Value": 9, "Docs": "" }, { "Name": "ValidationNone", "Value": 10, "Docs": "" }] }, "ThreadMode": { "Name": "ThreadMode", "Docs": "", "Values": [{ "Name": "ThreadOff", "Value": "off", "Docs": "" }, { "Name": "ThreadOn", "Value": "on", "Docs": "" }, { "Name": "ThreadUnread", "Value": "unread", "Docs": "" }] }, "AttachmentType": { "Name": "AttachmentType", "Docs": "", "Values": [{ "Name": "AttachmentIndifferent", "Value": "", "Docs": "" }, { "Name": "AttachmentNone", "Value": "none", "Docs": "" }, { "Name": "AttachmentAny", "Value": "any", "Docs": "" }, { "Name": "AttachmentImage", "Value": "image", "Docs": "" }, { "Name": "AttachmentPDF", "Value": "pdf", "Docs": "" }, { "Name": "AttachmentArchive", "Value": "archive", "Docs": "" }, { "Name": "AttachmentSpreadsheet", "Value": "spreadsheet", "Docs": "" }, { "Name": "AttachmentDocument", "Value": "document", "Docs": "" }, { "Name": "AttachmentPresentation", "Value": "presentation", "Docs": "" }] }, + "SecurityResult": { "Name": "SecurityResult", "Docs": "", "Values": [{ "Name": "SecurityResultError", "Value": "error", "Docs": "" }, { "Name": "SecurityResultNo", "Value": "no", "Docs": "" }, { "Name": "SecurityResultYes", "Value": "yes", "Docs": "" }, { "Name": "SecurityResultUnknown", "Value": "unknown", "Docs": "" }] }, "Localpart": { "Name": "Localpart", "Docs": "", "Values": null }, }; api.parser = { @@ -100,6 +113,7 @@ var api; File: (v) => api.parse("File", v), ForwardAttachments: (v) => api.parse("ForwardAttachments", v), Mailbox: (v) => api.parse("Mailbox", v), + RecipientSecurity: (v) => api.parse("RecipientSecurity", v), EventStart: (v) => api.parse("EventStart", v), DomainAddressConfig: (v) => api.parse("DomainAddressConfig", v), EventViewErr: (v) => api.parse("EventViewErr", v), @@ -127,6 +141,7 @@ var api; Validation: (v) => api.parse("Validation", v), ThreadMode: (v) => api.parse("ThreadMode", v), AttachmentType: (v) => api.parse("AttachmentType", v), + SecurityResult: (v) => api.parse("SecurityResult", v), Localpart: (v) => api.parse("Localpart", v), }; let defaultOptions = { slicesNullable: true, mapsNullable: true, nullableOptional: true }; @@ -290,6 +305,15 @@ var api; const params = [messageIDs, mute]; return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params); } + // RecipientSecurity looks up security properties of the address in the + // single-address message addressee (as it appears in a To/Cc/Bcc/etc header). + async RecipientSecurity(messageAddressee) { + const fn = "RecipientSecurity"; + const paramTypes = [["string"]]; + const returnTypes = [["RecipientSecurity"]]; + const params = [messageAddressee]; + return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params); + } // SSETypes exists to ensure the generated API contains the types, for use in SSE events. async SSETypes() { const fn = "SSETypes"; @@ -2043,8 +2067,55 @@ const compose = (opts) => { if (single && views.length !== 0) { return; } - let autosizeElem, inputElem; - const root = dom.span(autosizeElem = dom.span(dom._class('autosize'), inputElem = dom.input(focusPlaceholder('Jane '), style({ width: 'auto' }), attr.value(addr), newAddressComplete(), function keydown(e) { + let rcptSecPromise = null; + let rcptSecAddr = ''; + let rcptSecAborter = {}; + let autosizeElem, inputElem, securityBar; + const fetchRecipientSecurity = () => { + if (inputElem.value === rcptSecAddr) { + return; + } + securityBar.style.borderImage = ''; + rcptSecAddr = inputElem.value; + if (!inputElem.value) { + return; + } + if (rcptSecAborter.abort) { + rcptSecAborter.abort(); + rcptSecAborter.abort = undefined; + } + const color = (v) => { + if (v === api.SecurityResult.SecurityResultYes) { + return '#50c40f'; + } + else if (v === api.SecurityResult.SecurityResultNo) { + return '#e15d1c'; + } + else if (v === api.SecurityResult.SecurityResultUnknown) { + return 'white'; + } + return '#aaa'; + }; + const setBar = (c0, c1, c2) => { + const stops = [ + c0 + ' 0%', c0 + ' 32%', 'white 32%', 'white 33%', + c1 + ' 33%', c1 + ' 66%', 'white 66%', 'white 67%', + c2 + ' 67%', c2 + ' 100%', + ].join(', '); + securityBar.style.borderImage = 'linear-gradient(to right, ' + stops + ') 1'; + }; + const aborter = {}; + rcptSecAborter = aborter; + rcptSecPromise = client.withOptions({ aborter: aborter }).RecipientSecurity(inputElem.value); + rcptSecPromise.then((rs) => { + setBar(color(rs.MTASTS), color(rs.DNSSEC), color(rs.DANE)); + aborter.abort = undefined; + }, () => { + setBar('#888', '#888', '#888'); + aborter.abort = undefined; + }); + }; + const root = dom.span(autosizeElem = dom.span(dom._class('autosize'), inputElem = dom.input(focusPlaceholder('Jane '), style({ width: 'auto' }), attr.value(addr), newAddressComplete(), attr.title('The bars below the input field indicate security features of the recipient (domain):\n1. Delivery with STARTTLS and MTA-STS (PKIX/WebPKI) enforced.\n2. MX lookup resulted in DNSSEC-signed response.\n3. First delivery destination host has DANE, so STARTTLS is required.\n\nColors:\n- Red, not implemented/unsupported\n- Green, implemented/supported\n- Gray, error while determining\n- Absent/white, unknown or skipped (e.g. dane check skipped due to dnssec-lookup error)'), function keydown(e) { if (e.key === '-' && e.ctrlKey) { remove(); } @@ -2059,13 +2130,20 @@ const compose = (opts) => { }, function input() { // data-value is used for size of ::after css pseudo-element to stretch input field. autosizeElem.dataset.value = inputElem.value; - })), ' ', dom.clickbutton('-', style({ padding: '0 .25em' }), attr.arialabel('Remove address.'), attr.title('Remove address.'), function click() { + }, function change() { + fetchRecipientSecurity(); + }), securityBar = dom.span(dom._class('securitybar'), style({ + margin: '0 1px', + borderBottom: '1.5px solid', + borderBottomColor: 'transparent', + }))), ' ', dom.clickbutton('-', style({ padding: '0 .25em' }), attr.arialabel('Remove address.'), attr.title('Remove address.'), function click() { remove(); if (single && views.length === 0) { btn.style.display = ''; } }), ' '); autosizeElem.dataset.value = inputElem.value; + fetchRecipientSecurity(); const remove = () => { const i = views.indexOf(v); views.splice(i, 1); @@ -2154,7 +2232,7 @@ const compose = (opts) => { minWidth: '40em', maxWidth: '95vw', borderRadius: '.25em', - }), dom.form(fieldset = dom.fieldset(dom.table(style({ width: '100%' }), dom.tr(dom.td(style({ textAlign: 'right', color: '#555' }), dom.span('From:')), dom.td(dom.clickbutton('Cancel', style({ float: 'right' }), attr.title('Close window, discarding message.'), clickCmd(cmdCancel, shortcuts)), from = dom.select(attr.required(''), style({ width: 'auto' }), fromOptions), ' ', toBtn = dom.clickbutton('To', clickCmd(cmdAddTo, shortcuts)), ' ', ccBtn = dom.clickbutton('Cc', clickCmd(cmdAddCc, shortcuts)), ' ', bccBtn = dom.clickbutton('Bcc', clickCmd(cmdAddBcc, shortcuts)), ' ', replyToBtn = dom.clickbutton('ReplyTo', clickCmd(cmdReplyTo, shortcuts)), ' ', customFromBtn = dom.clickbutton('From', attr.title('Set custom From address/name.'), clickCmd(cmdCustomFrom, shortcuts)))), toRow = dom.tr(dom.td('To:', style({ textAlign: 'right', color: '#555' })), toCell = dom.td(style({ lineHeight: '1.5' }))), replyToRow = dom.tr(dom.td('Reply-To:', style({ textAlign: 'right', color: '#555' })), replyToCell = dom.td(style({ lineHeight: '1.5' }))), ccRow = dom.tr(dom.td('Cc:', style({ textAlign: 'right', color: '#555' })), ccCell = dom.td(style({ lineHeight: '1.5' }))), bccRow = dom.tr(dom.td('Bcc:', style({ textAlign: 'right', color: '#555' })), bccCell = dom.td(style({ lineHeight: '1.5' }))), dom.tr(dom.td('Subject:', style({ textAlign: 'right', color: '#555' })), dom.td(subjectAutosize = dom.span(dom._class('autosize'), style({ width: '100%' }), // Without 100% width, the span takes minimal width for input, we want the full table cell. + }), dom.form(fieldset = dom.fieldset(dom.table(style({ width: '100%' }), dom.tr(dom.td(style({ textAlign: 'right', color: '#555' }), dom.span('From:')), dom.td(dom.clickbutton('Cancel', style({ float: 'right', marginLeft: '1em', marginTop: '.15em' }), attr.title('Close window, discarding message.'), clickCmd(cmdCancel, shortcuts)), from = dom.select(attr.required(''), style({ width: 'auto' }), fromOptions), ' ', toBtn = dom.clickbutton('To', clickCmd(cmdAddTo, shortcuts)), ' ', ccBtn = dom.clickbutton('Cc', clickCmd(cmdAddCc, shortcuts)), ' ', bccBtn = dom.clickbutton('Bcc', clickCmd(cmdAddBcc, shortcuts)), ' ', replyToBtn = dom.clickbutton('ReplyTo', clickCmd(cmdReplyTo, shortcuts)), ' ', customFromBtn = dom.clickbutton('From', attr.title('Set custom From address/name.'), clickCmd(cmdCustomFrom, shortcuts)))), toRow = dom.tr(dom.td('To:', style({ textAlign: 'right', color: '#555' })), toCell = dom.td(style({ lineHeight: '1.5' }))), replyToRow = dom.tr(dom.td('Reply-To:', style({ textAlign: 'right', color: '#555' })), replyToCell = dom.td(style({ lineHeight: '1.5' }))), ccRow = dom.tr(dom.td('Cc:', style({ textAlign: 'right', color: '#555' })), ccCell = dom.td(style({ lineHeight: '1.5' }))), bccRow = dom.tr(dom.td('Bcc:', style({ textAlign: 'right', color: '#555' })), bccCell = dom.td(style({ lineHeight: '1.5' }))), dom.tr(dom.td('Subject:', style({ textAlign: 'right', color: '#555' })), dom.td(subjectAutosize = dom.span(dom._class('autosize'), style({ width: '100%' }), // Without 100% width, the span takes minimal width for input, we want the full table cell. subject = dom.input(style({ width: '100%' }), attr.value(opts.subject || ''), attr.required(''), focusPlaceholder('subject...'), function input() { subjectAutosize.dataset.value = subject.value; }))))), body = dom.textarea(dom._class('mono'), attr.rows('15'), style({ width: '100%' }), diff --git a/webmail/webmail.ts b/webmail/webmail.ts index 65ac444885..c7d6569dae 100644 --- a/webmail/webmail.ts +++ b/webmail/webmail.ts @@ -1254,7 +1254,58 @@ const compose = (opts: ComposeOptions) => { return } - let autosizeElem: HTMLElement, inputElem: HTMLInputElement + let rcptSecPromise: Promise | null = null + let rcptSecAddr: string = '' + let rcptSecAborter: {abort?: () => void} = {} + + let autosizeElem: HTMLElement, inputElem: HTMLInputElement, securityBar: HTMLElement + + const fetchRecipientSecurity = () => { + if (inputElem.value === rcptSecAddr) { + return + } + securityBar.style.borderImage = '' + rcptSecAddr = inputElem.value + if (!inputElem.value) { + return + } + + if (rcptSecAborter.abort) { + rcptSecAborter.abort() + rcptSecAborter.abort = undefined + } + + const color = (v: api.SecurityResult) => { + if (v === api.SecurityResult.SecurityResultYes) { + return '#50c40f' + } else if (v === api.SecurityResult.SecurityResultNo) { + return '#e15d1c' + } else if (v === api.SecurityResult.SecurityResultUnknown) { + return 'white' + } + return '#aaa' + } + const setBar = (c0: string, c1: string, c2: string) => { + const stops = [ + c0 + ' 0%', c0 + ' 32%', 'white 32%', 'white 33%', + c1 + ' 33%', c1 + ' 66%', 'white 66%', 'white 67%', + c2 + ' 67%', c2 + ' 100%', + ].join(', ') + securityBar.style.borderImage = 'linear-gradient(to right, ' + stops + ') 1' + } + + const aborter: {abort?: () => void} = {} + rcptSecAborter = aborter + rcptSecPromise = client.withOptions({aborter: aborter}).RecipientSecurity(inputElem.value) + rcptSecPromise.then((rs) => { + setBar(color(rs.MTASTS), color(rs.DNSSEC), color(rs.DANE)) + aborter.abort = undefined + }, () => { + setBar('#888', '#888', '#888') + aborter.abort = undefined + }) + } + const root = dom.span( autosizeElem=dom.span( dom._class('autosize'), @@ -1263,6 +1314,7 @@ const compose = (opts: ComposeOptions) => { style({width: 'auto'}), attr.value(addr), newAddressComplete(), + attr.title('The bars below the input field indicate security features of the recipient (domain):\n1. Delivery with STARTTLS and MTA-STS (PKIX/WebPKI) enforced.\n2. MX lookup resulted in DNSSEC-signed response.\n3. First delivery destination host has DANE, so STARTTLS is required.\n\nColors:\n- Red, not implemented/unsupported\n- Green, implemented/supported\n- Gray, error while determining\n- Absent/white, unknown or skipped (e.g. dane check skipped due to dnssec-lookup error)'), function keydown(e: KeyboardEvent) { if (e.key === '-' && e.ctrlKey) { remove() @@ -1278,6 +1330,17 @@ const compose = (opts: ComposeOptions) => { // data-value is used for size of ::after css pseudo-element to stretch input field. autosizeElem.dataset.value = inputElem.value }, + function change() { + fetchRecipientSecurity() + }, + ), + securityBar=dom.span( + dom._class('securitybar'), + style({ + margin: '0 1px', + borderBottom: '1.5px solid', + borderBottomColor: 'transparent', + }), ), ), ' ', @@ -1290,6 +1353,7 @@ const compose = (opts: ComposeOptions) => { ' ', ) autosizeElem.dataset.value = inputElem.value + fetchRecipientSecurity() const remove = () => { const i = views.indexOf(v) @@ -1397,7 +1461,7 @@ const compose = (opts: ComposeOptions) => { dom.span('From:'), ), dom.td( - dom.clickbutton('Cancel', style({float: 'right'}), attr.title('Close window, discarding message.'), clickCmd(cmdCancel, shortcuts)), + dom.clickbutton('Cancel', style({float: 'right', marginLeft: '1em', marginTop: '.15em'}), attr.title('Close window, discarding message.'), clickCmd(cmdCancel, shortcuts)), from=dom.select( attr.required(''), style({width: 'auto'}),