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.
This commit is contained in:
Mechiel Lukkien 2023-10-15 15:05:20 +02:00
parent 4ab3e6bc9b
commit 08995c7806
No known key found for this signature in database
9 changed files with 445 additions and 15 deletions

View file

@ -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)

View file

@ -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 {

View file

@ -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.",

View file

@ -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<RecipientSecurity> {
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"

View file

@ -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 <mjl+cc2@mox.example>"})
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})
}

View file

@ -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";

View file

@ -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";

View file

@ -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 <jane@example.org>'), 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 <jane@example.org>'), 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%' }),

View file

@ -1254,7 +1254,58 @@ const compose = (opts: ComposeOptions) => {
return
}
let autosizeElem: HTMLElement, inputElem: HTMLInputElement
let rcptSecPromise: Promise<api.RecipientSecurity> | 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'}),