diff --git a/mtastsdb/db.go b/mtastsdb/db.go index 7b5f262..cef3828 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 47a8c5c..0133459 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 7663876..7598ca5 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 6c80836..19e089e 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 d6a98e4..dde7a11 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 2e40686..748b03c 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 e82608c..cdfaea8 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 a2d4d65..7517628 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 65ac444..c7d6569 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'}),