2023-01-30 16:27:06 +03:00
package imapserver
// todo: if fetch fails part-way through the command, we wouldn't be storing the messages that were parsed. should we try harder to get parsed form of messages stored in db?
import (
"bytes"
"errors"
"fmt"
"io"
2024-02-08 16:49:01 +03:00
"log/slog"
2023-01-30 16:27:06 +03:00
"net/textproto"
"sort"
"strings"
2023-07-24 22:21:05 +03:00
"golang.org/x/exp/maps"
2023-01-30 16:27:06 +03:00
"github.com/mjl-/bstore"
"github.com/mjl-/mox/message"
2023-12-05 23:13:57 +03:00
"github.com/mjl-/mox/mox-"
2023-01-30 16:27:06 +03:00
"github.com/mjl-/mox/moxio"
"github.com/mjl-/mox/store"
)
// functions to handle fetch attribute requests are defined on fetchCmd.
type fetchCmd struct {
2023-07-24 22:21:05 +03:00
conn * conn
mailboxID int64
uid store . UID
tx * bstore . Tx // Writable tx, for storing message when first parsed as mime parts.
changes [ ] store . Change // For updated Seen flag.
markSeen bool
needFlags bool
add webmail
it was far down on the roadmap, but implemented earlier, because it's
interesting, and to help prepare for a jmap implementation. for jmap we need to
implement more client-like functionality than with just imap. internal data
structures need to change. jmap has lots of other requirements, so it's already
a big project. by implementing a webmail now, some of the required data
structure changes become clear and can be made now, so the later jmap
implementation can do things similarly to the webmail code. the webmail
frontend and webmail are written together, making their interface/api much
smaller and simpler than jmap.
one of the internal changes is that we now keep track of per-mailbox
total/unread/unseen/deleted message counts and mailbox sizes. keeping this
data consistent after any change to the stored messages (through the code base)
is tricky, so mox now has a consistency check that verifies the counts are
correct, which runs only during tests, each time an internal account reference
is closed. we have a few more internal "changes" that are propagated for the
webmail frontend (that imap doesn't have a way to propagate on a connection),
like changes to the special-use flags on mailboxes, and used keywords in a
mailbox. more changes that will be required have revealed themselves while
implementing the webmail, and will be implemented next.
the webmail user interface is modeled after the mail clients i use or have
used: thunderbird, macos mail, mutt; and webmails i normally only use for
testing: gmail, proton, yahoo, outlook. a somewhat technical user is assumed,
but still the goal is to make this webmail client easy to use for everyone. the
user interface looks like most other mail clients: a list of mailboxes, a
search bar, a message list view, and message details. there is a top/bottom and
a left/right layout for the list/message view, default is automatic based on
screen size. the panes can be resized by the user. buttons for actions are just
text, not icons. clicking a button briefly shows the shortcut for the action in
the bottom right, helping with learning to operate quickly. any text that is
underdotted has a title attribute that causes more information to be displayed,
e.g. what a button does or a field is about. to highlight potential phishing
attempts, any text (anywhere in the webclient) that switches unicode "blocks"
(a rough approximation to (language) scripts) within a word is underlined
orange. multiple messages can be selected with familiar ui interaction:
clicking while holding control and/or shift keys. keyboard navigation works
with arrows/page up/down and home/end keys, and also with a few basic vi-like
keys for list/message navigation. we prefer showing the text instead of
html (with inlined images only) version of a message. html messages are shown
in an iframe served from an endpoint with CSP headers to prevent dangerous
resources (scripts, external images) from being loaded. the html is also
sanitized, with javascript removed. a user can choose to load external
resources (e.g. images for tracking purposes).
the frontend is just (strict) typescript, no external frameworks. all
incoming/outgoing data is typechecked, both the api request parameters and
response types, and the data coming in over SSE. the types and checking code
are generated with sherpats, which uses the api definitions generated by
sherpadoc based on the Go code. so types from the backend are automatically
propagated to the frontend. since there is no framework to automatically
propagate properties and rerender components, changes coming in over the SSE
connection are propagated explicitly with regular function calls. the ui is
separated into "views", each with a "root" dom element that is added to the
visible document. these views have additional functions for getting changes
propagated, often resulting in the view updating its (internal) ui state (dom).
we keep the frontend compilation simple, it's just a few typescript files that
get compiled (combined and types stripped) into a single js file, no additional
runtime code needed or complicated build processes used. the webmail is served
is served from a compressed, cachable html file that includes style and the
javascript, currently just over 225kb uncompressed, under 60kb compressed (not
minified, including comments). we include the generated js files in the
repository, to keep Go's easily buildable self-contained binaries.
authentication is basic http, as with the account and admin pages. most data
comes in over one long-term SSE connection to the backend. api requests signal
which mailbox/search/messages are requested over the SSE connection. fetching
individual messages, and making changes, are done through api calls. the
operations are similar to imap, so some code has been moved from package
imapserver to package store. the future jmap implementation will benefit from
these changes too. more functionality will probably be moved to the store
package in the future.
the quickstart enables webmail on the internal listener by default (for new
installs). users can enable it on the public listener if they want to. mox
localserve enables it too. to enable webmail on existing installs, add settings
like the following to the listeners in mox.conf, similar to AccountHTTP(S):
WebmailHTTP:
Enabled: true
WebmailHTTPS:
Enabled: true
special thanks to liesbeth, gerben, andrii for early user feedback.
there is plenty still to do, see the list at the top of webmail/webmail.ts.
feedback welcome as always.
2023-08-07 22:57:03 +03:00
needModseq bool // Whether untagged responses needs modseq.
expungeIssued bool // Set if a message cannot be read. Can happen for expunged messages.
modseq store . ModSeq // Initialized on first change, for marking messages as seen.
isUID bool // If this is a UID FETCH command.
hasChangedSince bool // Whether CHANGEDSINCE was set. Enables MODSEQ in response.
deltaCounts store . MailboxCounts // By marking \Seen, the number of unread/unseen messages will go down. We update counts at the end.
2023-01-30 16:27:06 +03:00
// Loaded when first needed, closed when message was processed.
m * store . Message // Message currently being processed.
msgr * store . MsgReader
part * message . Part
}
// error when processing an attribute. we typically just don't respond with requested attributes that encounter a failure.
type attrError struct { err error }
func ( e attrError ) Error ( ) string {
return e . err . Error ( )
}
// raise error processing an attribute.
func ( cmd * fetchCmd ) xerrorf ( format string , args ... any ) {
panic ( attrError { fmt . Errorf ( format , args ... ) } )
}
func ( cmd * fetchCmd ) xcheckf ( err error , format string , args ... any ) {
if err != nil {
msg := fmt . Sprintf ( format , args ... )
cmd . xerrorf ( "%s: %w" , msg , err )
}
}
// Fetch returns information about messages, be it email envelopes, headers,
// bodies, full messages, flags.
//
// State: Selected
func ( c * conn ) cmdxFetch ( isUID bool , tag , cmdstr string , p * parser ) {
2023-07-24 22:21:05 +03:00
// Command: ../rfc/9051:4330 ../rfc/3501:2992 ../rfc/7162:864
// Examples: ../rfc/9051:4463 ../rfc/9051:4520 ../rfc/7162:880
// Response syntax: ../rfc/9051:6742 ../rfc/3501:4864 ../rfc/7162:2490
2023-01-30 16:27:06 +03:00
2023-07-24 22:21:05 +03:00
// Request syntax: ../rfc/9051:6553 ../rfc/3501:4748 ../rfc/4466:535 ../rfc/7162:2475
2023-01-30 16:27:06 +03:00
p . xspace ( )
nums := p . xnumSet ( )
p . xspace ( )
2023-07-24 22:21:05 +03:00
atts := p . xfetchAtts ( isUID )
var changedSince int64
var haveChangedSince bool
var vanished bool
if p . space ( ) {
// ../rfc/4466:542
// ../rfc/7162:2479
p . xtake ( "(" )
seen := map [ string ] bool { }
for {
var w string
if isUID && p . conn . enabled [ capQresync ] {
// Vanished only valid for uid fetch, and only for qresync. ../rfc/7162:1693
w = p . xtakelist ( "CHANGEDSINCE" , "VANISHED" )
} else {
w = p . xtakelist ( "CHANGEDSINCE" )
}
if seen [ w ] {
xsyntaxErrorf ( "duplicate fetch modifier %s" , w )
}
seen [ w ] = true
switch w {
case "CHANGEDSINCE" :
p . xspace ( )
changedSince = p . xnumber64 ( )
// workaround: ios mail (16.5.1) was seen sending changedSince 0 on an existing account that got condstore enabled.
2023-12-05 23:13:57 +03:00
if changedSince == 0 && mox . Pedantic {
2023-07-24 22:21:05 +03:00
// ../rfc/7162:2551
xsyntaxErrorf ( "changedsince modseq must be > 0" )
}
// CHANGEDSINCE is a CONDSTORE-enabling parameter. ../rfc/7162:380
p . conn . xensureCondstore ( nil )
haveChangedSince = true
case "VANISHED" :
vanished = true
}
if p . take ( ")" ) {
break
}
p . xspace ( )
}
// ../rfc/7162:1701
if vanished && ! haveChangedSince {
xsyntaxErrorf ( "VANISHED can only be used with CHANGEDSINCE" )
}
}
2023-01-30 16:27:06 +03:00
p . xempty ( )
// We don't use c.account.WithRLock because we write to the client while reading messages.
// We get the rlock, then we check the mailbox, release the lock and read the messages.
// The db transaction still locks out any changes to the database...
c . account . RLock ( )
runlock := c . account . RUnlock
// Note: we call runlock in a closure because we replace it below.
defer func ( ) {
runlock ( )
} ( )
2023-07-24 22:21:05 +03:00
var vanishedUIDs [ ] store . UID
cmd := & fetchCmd { conn : c , mailboxID : c . mailboxID , isUID : isUID , hasChangedSince : haveChangedSince }
2023-01-30 16:27:06 +03:00
c . xdbwrite ( func ( tx * bstore . Tx ) {
cmd . tx = tx
// Ensure the mailbox still exists.
add webmail
it was far down on the roadmap, but implemented earlier, because it's
interesting, and to help prepare for a jmap implementation. for jmap we need to
implement more client-like functionality than with just imap. internal data
structures need to change. jmap has lots of other requirements, so it's already
a big project. by implementing a webmail now, some of the required data
structure changes become clear and can be made now, so the later jmap
implementation can do things similarly to the webmail code. the webmail
frontend and webmail are written together, making their interface/api much
smaller and simpler than jmap.
one of the internal changes is that we now keep track of per-mailbox
total/unread/unseen/deleted message counts and mailbox sizes. keeping this
data consistent after any change to the stored messages (through the code base)
is tricky, so mox now has a consistency check that verifies the counts are
correct, which runs only during tests, each time an internal account reference
is closed. we have a few more internal "changes" that are propagated for the
webmail frontend (that imap doesn't have a way to propagate on a connection),
like changes to the special-use flags on mailboxes, and used keywords in a
mailbox. more changes that will be required have revealed themselves while
implementing the webmail, and will be implemented next.
the webmail user interface is modeled after the mail clients i use or have
used: thunderbird, macos mail, mutt; and webmails i normally only use for
testing: gmail, proton, yahoo, outlook. a somewhat technical user is assumed,
but still the goal is to make this webmail client easy to use for everyone. the
user interface looks like most other mail clients: a list of mailboxes, a
search bar, a message list view, and message details. there is a top/bottom and
a left/right layout for the list/message view, default is automatic based on
screen size. the panes can be resized by the user. buttons for actions are just
text, not icons. clicking a button briefly shows the shortcut for the action in
the bottom right, helping with learning to operate quickly. any text that is
underdotted has a title attribute that causes more information to be displayed,
e.g. what a button does or a field is about. to highlight potential phishing
attempts, any text (anywhere in the webclient) that switches unicode "blocks"
(a rough approximation to (language) scripts) within a word is underlined
orange. multiple messages can be selected with familiar ui interaction:
clicking while holding control and/or shift keys. keyboard navigation works
with arrows/page up/down and home/end keys, and also with a few basic vi-like
keys for list/message navigation. we prefer showing the text instead of
html (with inlined images only) version of a message. html messages are shown
in an iframe served from an endpoint with CSP headers to prevent dangerous
resources (scripts, external images) from being loaded. the html is also
sanitized, with javascript removed. a user can choose to load external
resources (e.g. images for tracking purposes).
the frontend is just (strict) typescript, no external frameworks. all
incoming/outgoing data is typechecked, both the api request parameters and
response types, and the data coming in over SSE. the types and checking code
are generated with sherpats, which uses the api definitions generated by
sherpadoc based on the Go code. so types from the backend are automatically
propagated to the frontend. since there is no framework to automatically
propagate properties and rerender components, changes coming in over the SSE
connection are propagated explicitly with regular function calls. the ui is
separated into "views", each with a "root" dom element that is added to the
visible document. these views have additional functions for getting changes
propagated, often resulting in the view updating its (internal) ui state (dom).
we keep the frontend compilation simple, it's just a few typescript files that
get compiled (combined and types stripped) into a single js file, no additional
runtime code needed or complicated build processes used. the webmail is served
is served from a compressed, cachable html file that includes style and the
javascript, currently just over 225kb uncompressed, under 60kb compressed (not
minified, including comments). we include the generated js files in the
repository, to keep Go's easily buildable self-contained binaries.
authentication is basic http, as with the account and admin pages. most data
comes in over one long-term SSE connection to the backend. api requests signal
which mailbox/search/messages are requested over the SSE connection. fetching
individual messages, and making changes, are done through api calls. the
operations are similar to imap, so some code has been moved from package
imapserver to package store. the future jmap implementation will benefit from
these changes too. more functionality will probably be moved to the store
package in the future.
the quickstart enables webmail on the internal listener by default (for new
installs). users can enable it on the public listener if they want to. mox
localserve enables it too. to enable webmail on existing installs, add settings
like the following to the listeners in mox.conf, similar to AccountHTTP(S):
WebmailHTTP:
Enabled: true
WebmailHTTPS:
Enabled: true
special thanks to liesbeth, gerben, andrii for early user feedback.
there is plenty still to do, see the list at the top of webmail/webmail.ts.
feedback welcome as always.
2023-08-07 22:57:03 +03:00
mb := c . xmailboxID ( tx , c . mailboxID )
2023-01-30 16:27:06 +03:00
2023-07-24 22:21:05 +03:00
var uids [ ] store . UID
// With changedSince, the client is likely asking for a small set of changes. Use a
// database query to trim down the uids we need to look at.
// ../rfc/7162:871
if changedSince > 0 {
q := bstore . QueryTx [ store . Message ] ( tx )
q . FilterNonzero ( store . Message { MailboxID : c . mailboxID } )
q . FilterGreater ( "ModSeq" , store . ModSeqFromClient ( changedSince ) )
if ! vanished {
q . FilterEqual ( "Expunged" , false )
}
err := q . ForEach ( func ( m store . Message ) error {
if m . Expunged {
vanishedUIDs = append ( vanishedUIDs , m . UID )
} else if isUID {
if nums . containsUID ( m . UID , c . uids , c . searchResult ) {
uids = append ( uids , m . UID )
}
} else {
seq := c . sequence ( m . UID )
if seq > 0 && nums . containsSeq ( seq , c . uids , c . searchResult ) {
uids = append ( uids , m . UID )
}
}
return nil
} )
xcheckf ( err , "looking up messages with changedsince" )
} else {
uids = c . xnumSetUIDs ( isUID , nums )
}
// Send vanished for all missing requested UIDs. ../rfc/7162:1718
if vanished {
delModSeq , err := c . account . HighestDeletedModSeq ( tx )
xcheckf ( err , "looking up highest deleted modseq" )
if changedSince < delModSeq . Client ( ) {
// First sort the uids we already found, for fast lookup.
sort . Slice ( vanishedUIDs , func ( i , j int ) bool {
return vanishedUIDs [ i ] < vanishedUIDs [ j ]
} )
// We'll be gathering any more vanished uids in more.
more := map [ store . UID ] struct { } { }
checkVanished := func ( uid store . UID ) {
if uidSearch ( c . uids , uid ) <= 0 && uidSearch ( vanishedUIDs , uid ) <= 0 {
more [ uid ] = struct { } { }
}
}
// Now look through the requested uids. We may have a searchResult, handle it
// separately from a numset with potential stars, over which we can more easily
// iterate.
if nums . searchResult {
for _ , uid := range c . searchResult {
checkVanished ( uid )
}
} else {
iter := nums . interpretStar ( c . uids ) . newIter ( )
for {
num , ok := iter . Next ( )
if ! ok {
break
}
checkVanished ( store . UID ( num ) )
}
}
vanishedUIDs = append ( vanishedUIDs , maps . Keys ( more ) ... )
}
}
2023-01-30 16:27:06 +03:00
// Release the account lock.
runlock ( )
runlock = func ( ) { } // Prevent defer from unlocking again.
2023-07-24 22:21:05 +03:00
// First report all vanished UIDs. ../rfc/7162:1714
if len ( vanishedUIDs ) > 0 {
// Mention all vanished UIDs in compact numset form.
// ../rfc/7162:1985
sort . Slice ( vanishedUIDs , func ( i , j int ) bool {
return vanishedUIDs [ i ] < vanishedUIDs [ j ]
} )
// No hard limit on response sizes, but clients are recommended to not send more
// than 8k. We send a more conservative max 4k.
for _ , s := range compactUIDSet ( vanishedUIDs ) . Strings ( 4 * 1024 - 32 ) {
c . bwritelinef ( "* VANISHED (EARLIER) %s" , s )
}
}
2023-01-30 16:27:06 +03:00
for _ , uid := range uids {
cmd . uid = uid
2023-12-05 15:35:58 +03:00
cmd . conn . log . Debug ( "processing uid" , slog . Any ( "uid" , uid ) )
2023-01-30 16:27:06 +03:00
cmd . process ( atts )
}
add webmail
it was far down on the roadmap, but implemented earlier, because it's
interesting, and to help prepare for a jmap implementation. for jmap we need to
implement more client-like functionality than with just imap. internal data
structures need to change. jmap has lots of other requirements, so it's already
a big project. by implementing a webmail now, some of the required data
structure changes become clear and can be made now, so the later jmap
implementation can do things similarly to the webmail code. the webmail
frontend and webmail are written together, making their interface/api much
smaller and simpler than jmap.
one of the internal changes is that we now keep track of per-mailbox
total/unread/unseen/deleted message counts and mailbox sizes. keeping this
data consistent after any change to the stored messages (through the code base)
is tricky, so mox now has a consistency check that verifies the counts are
correct, which runs only during tests, each time an internal account reference
is closed. we have a few more internal "changes" that are propagated for the
webmail frontend (that imap doesn't have a way to propagate on a connection),
like changes to the special-use flags on mailboxes, and used keywords in a
mailbox. more changes that will be required have revealed themselves while
implementing the webmail, and will be implemented next.
the webmail user interface is modeled after the mail clients i use or have
used: thunderbird, macos mail, mutt; and webmails i normally only use for
testing: gmail, proton, yahoo, outlook. a somewhat technical user is assumed,
but still the goal is to make this webmail client easy to use for everyone. the
user interface looks like most other mail clients: a list of mailboxes, a
search bar, a message list view, and message details. there is a top/bottom and
a left/right layout for the list/message view, default is automatic based on
screen size. the panes can be resized by the user. buttons for actions are just
text, not icons. clicking a button briefly shows the shortcut for the action in
the bottom right, helping with learning to operate quickly. any text that is
underdotted has a title attribute that causes more information to be displayed,
e.g. what a button does or a field is about. to highlight potential phishing
attempts, any text (anywhere in the webclient) that switches unicode "blocks"
(a rough approximation to (language) scripts) within a word is underlined
orange. multiple messages can be selected with familiar ui interaction:
clicking while holding control and/or shift keys. keyboard navigation works
with arrows/page up/down and home/end keys, and also with a few basic vi-like
keys for list/message navigation. we prefer showing the text instead of
html (with inlined images only) version of a message. html messages are shown
in an iframe served from an endpoint with CSP headers to prevent dangerous
resources (scripts, external images) from being loaded. the html is also
sanitized, with javascript removed. a user can choose to load external
resources (e.g. images for tracking purposes).
the frontend is just (strict) typescript, no external frameworks. all
incoming/outgoing data is typechecked, both the api request parameters and
response types, and the data coming in over SSE. the types and checking code
are generated with sherpats, which uses the api definitions generated by
sherpadoc based on the Go code. so types from the backend are automatically
propagated to the frontend. since there is no framework to automatically
propagate properties and rerender components, changes coming in over the SSE
connection are propagated explicitly with regular function calls. the ui is
separated into "views", each with a "root" dom element that is added to the
visible document. these views have additional functions for getting changes
propagated, often resulting in the view updating its (internal) ui state (dom).
we keep the frontend compilation simple, it's just a few typescript files that
get compiled (combined and types stripped) into a single js file, no additional
runtime code needed or complicated build processes used. the webmail is served
is served from a compressed, cachable html file that includes style and the
javascript, currently just over 225kb uncompressed, under 60kb compressed (not
minified, including comments). we include the generated js files in the
repository, to keep Go's easily buildable self-contained binaries.
authentication is basic http, as with the account and admin pages. most data
comes in over one long-term SSE connection to the backend. api requests signal
which mailbox/search/messages are requested over the SSE connection. fetching
individual messages, and making changes, are done through api calls. the
operations are similar to imap, so some code has been moved from package
imapserver to package store. the future jmap implementation will benefit from
these changes too. more functionality will probably be moved to the store
package in the future.
the quickstart enables webmail on the internal listener by default (for new
installs). users can enable it on the public listener if they want to. mox
localserve enables it too. to enable webmail on existing installs, add settings
like the following to the listeners in mox.conf, similar to AccountHTTP(S):
WebmailHTTP:
Enabled: true
WebmailHTTPS:
Enabled: true
special thanks to liesbeth, gerben, andrii for early user feedback.
there is plenty still to do, see the list at the top of webmail/webmail.ts.
feedback welcome as always.
2023-08-07 22:57:03 +03:00
var zeromc store . MailboxCounts
if cmd . deltaCounts != zeromc {
mb . Add ( cmd . deltaCounts ) // Unseen/Unread will be <= 0.
err := tx . Update ( & mb )
xcheckf ( err , "updating mailbox counts" )
cmd . changes = append ( cmd . changes , mb . ChangeCounts ( ) )
2023-12-20 22:54:12 +03:00
// No need to update account total message size.
add webmail
it was far down on the roadmap, but implemented earlier, because it's
interesting, and to help prepare for a jmap implementation. for jmap we need to
implement more client-like functionality than with just imap. internal data
structures need to change. jmap has lots of other requirements, so it's already
a big project. by implementing a webmail now, some of the required data
structure changes become clear and can be made now, so the later jmap
implementation can do things similarly to the webmail code. the webmail
frontend and webmail are written together, making their interface/api much
smaller and simpler than jmap.
one of the internal changes is that we now keep track of per-mailbox
total/unread/unseen/deleted message counts and mailbox sizes. keeping this
data consistent after any change to the stored messages (through the code base)
is tricky, so mox now has a consistency check that verifies the counts are
correct, which runs only during tests, each time an internal account reference
is closed. we have a few more internal "changes" that are propagated for the
webmail frontend (that imap doesn't have a way to propagate on a connection),
like changes to the special-use flags on mailboxes, and used keywords in a
mailbox. more changes that will be required have revealed themselves while
implementing the webmail, and will be implemented next.
the webmail user interface is modeled after the mail clients i use or have
used: thunderbird, macos mail, mutt; and webmails i normally only use for
testing: gmail, proton, yahoo, outlook. a somewhat technical user is assumed,
but still the goal is to make this webmail client easy to use for everyone. the
user interface looks like most other mail clients: a list of mailboxes, a
search bar, a message list view, and message details. there is a top/bottom and
a left/right layout for the list/message view, default is automatic based on
screen size. the panes can be resized by the user. buttons for actions are just
text, not icons. clicking a button briefly shows the shortcut for the action in
the bottom right, helping with learning to operate quickly. any text that is
underdotted has a title attribute that causes more information to be displayed,
e.g. what a button does or a field is about. to highlight potential phishing
attempts, any text (anywhere in the webclient) that switches unicode "blocks"
(a rough approximation to (language) scripts) within a word is underlined
orange. multiple messages can be selected with familiar ui interaction:
clicking while holding control and/or shift keys. keyboard navigation works
with arrows/page up/down and home/end keys, and also with a few basic vi-like
keys for list/message navigation. we prefer showing the text instead of
html (with inlined images only) version of a message. html messages are shown
in an iframe served from an endpoint with CSP headers to prevent dangerous
resources (scripts, external images) from being loaded. the html is also
sanitized, with javascript removed. a user can choose to load external
resources (e.g. images for tracking purposes).
the frontend is just (strict) typescript, no external frameworks. all
incoming/outgoing data is typechecked, both the api request parameters and
response types, and the data coming in over SSE. the types and checking code
are generated with sherpats, which uses the api definitions generated by
sherpadoc based on the Go code. so types from the backend are automatically
propagated to the frontend. since there is no framework to automatically
propagate properties and rerender components, changes coming in over the SSE
connection are propagated explicitly with regular function calls. the ui is
separated into "views", each with a "root" dom element that is added to the
visible document. these views have additional functions for getting changes
propagated, often resulting in the view updating its (internal) ui state (dom).
we keep the frontend compilation simple, it's just a few typescript files that
get compiled (combined and types stripped) into a single js file, no additional
runtime code needed or complicated build processes used. the webmail is served
is served from a compressed, cachable html file that includes style and the
javascript, currently just over 225kb uncompressed, under 60kb compressed (not
minified, including comments). we include the generated js files in the
repository, to keep Go's easily buildable self-contained binaries.
authentication is basic http, as with the account and admin pages. most data
comes in over one long-term SSE connection to the backend. api requests signal
which mailbox/search/messages are requested over the SSE connection. fetching
individual messages, and making changes, are done through api calls. the
operations are similar to imap, so some code has been moved from package
imapserver to package store. the future jmap implementation will benefit from
these changes too. more functionality will probably be moved to the store
package in the future.
the quickstart enables webmail on the internal listener by default (for new
installs). users can enable it on the public listener if they want to. mox
localserve enables it too. to enable webmail on existing installs, add settings
like the following to the listeners in mox.conf, similar to AccountHTTP(S):
WebmailHTTP:
Enabled: true
WebmailHTTPS:
Enabled: true
special thanks to liesbeth, gerben, andrii for early user feedback.
there is plenty still to do, see the list at the top of webmail/webmail.ts.
feedback welcome as always.
2023-08-07 22:57:03 +03:00
}
2023-01-30 16:27:06 +03:00
} )
if len ( cmd . changes ) > 0 {
// Broadcast seen updates to other connections.
c . broadcast ( cmd . changes )
}
if cmd . expungeIssued {
// ../rfc/2180:343
c . writeresultf ( "%s NO [EXPUNGEISSUED] at least one message was expunged" , tag )
} else {
c . ok ( tag , cmdstr )
}
}
2023-07-24 22:21:05 +03:00
func ( cmd * fetchCmd ) xmodseq ( ) store . ModSeq {
if cmd . modseq == 0 {
var err error
cmd . modseq , err = cmd . conn . account . NextModSeq ( cmd . tx )
cmd . xcheckf ( err , "assigning next modseq" )
}
return cmd . modseq
}
2023-01-30 16:27:06 +03:00
func ( cmd * fetchCmd ) xensureMessage ( ) * store . Message {
if cmd . m != nil {
return cmd . m
}
q := bstore . QueryTx [ store . Message ] ( cmd . tx )
q . FilterNonzero ( store . Message { MailboxID : cmd . mailboxID , UID : cmd . uid } )
2023-07-24 22:21:05 +03:00
q . FilterEqual ( "Expunged" , false )
2023-01-30 16:27:06 +03:00
m , err := q . Get ( )
cmd . xcheckf ( err , "get message for uid %d" , cmd . uid )
cmd . m = & m
return cmd . m
}
func ( cmd * fetchCmd ) xensureParsed ( ) ( * store . MsgReader , * message . Part ) {
if cmd . msgr != nil {
return cmd . msgr , cmd . part
}
m := cmd . xensureMessage ( )
cmd . msgr = cmd . conn . account . MessageReader ( * m )
defer func ( ) {
if cmd . part == nil {
err := cmd . msgr . Close ( )
cmd . conn . xsanity ( err , "closing messagereader" )
cmd . msgr = nil
}
} ( )
p , err := m . LoadPart ( cmd . msgr )
xcheckf ( err , "load parsed message" )
cmd . part = & p
return cmd . msgr , cmd . part
}
func ( cmd * fetchCmd ) process ( atts [ ] fetchAtt ) {
defer func ( ) {
cmd . m = nil
cmd . part = nil
if cmd . msgr != nil {
err := cmd . msgr . Close ( )
cmd . conn . xsanity ( err , "closing messagereader" )
cmd . msgr = nil
}
x := recover ( )
if x == nil {
return
}
err , ok := x . ( attrError )
if ! ok {
panic ( x )
}
if errors . Is ( err , bstore . ErrAbsent ) {
cmd . expungeIssued = true
return
}
2023-12-05 15:35:58 +03:00
cmd . conn . log . Infox ( "processing fetch attribute" , err , slog . Any ( "uid" , cmd . uid ) )
2023-01-30 16:27:06 +03:00
xuserErrorf ( "processing fetch attribute: %v" , err )
} ( )
data := listspace { bare ( "UID" ) , number ( cmd . uid ) }
cmd . markSeen = false
cmd . needFlags = false
2023-07-24 22:21:05 +03:00
cmd . needModseq = false
2023-01-30 16:27:06 +03:00
for _ , a := range atts {
data = append ( data , cmd . xprocessAtt ( a ) ... )
}
if cmd . markSeen {
m := cmd . xensureMessage ( )
add webmail
it was far down on the roadmap, but implemented earlier, because it's
interesting, and to help prepare for a jmap implementation. for jmap we need to
implement more client-like functionality than with just imap. internal data
structures need to change. jmap has lots of other requirements, so it's already
a big project. by implementing a webmail now, some of the required data
structure changes become clear and can be made now, so the later jmap
implementation can do things similarly to the webmail code. the webmail
frontend and webmail are written together, making their interface/api much
smaller and simpler than jmap.
one of the internal changes is that we now keep track of per-mailbox
total/unread/unseen/deleted message counts and mailbox sizes. keeping this
data consistent after any change to the stored messages (through the code base)
is tricky, so mox now has a consistency check that verifies the counts are
correct, which runs only during tests, each time an internal account reference
is closed. we have a few more internal "changes" that are propagated for the
webmail frontend (that imap doesn't have a way to propagate on a connection),
like changes to the special-use flags on mailboxes, and used keywords in a
mailbox. more changes that will be required have revealed themselves while
implementing the webmail, and will be implemented next.
the webmail user interface is modeled after the mail clients i use or have
used: thunderbird, macos mail, mutt; and webmails i normally only use for
testing: gmail, proton, yahoo, outlook. a somewhat technical user is assumed,
but still the goal is to make this webmail client easy to use for everyone. the
user interface looks like most other mail clients: a list of mailboxes, a
search bar, a message list view, and message details. there is a top/bottom and
a left/right layout for the list/message view, default is automatic based on
screen size. the panes can be resized by the user. buttons for actions are just
text, not icons. clicking a button briefly shows the shortcut for the action in
the bottom right, helping with learning to operate quickly. any text that is
underdotted has a title attribute that causes more information to be displayed,
e.g. what a button does or a field is about. to highlight potential phishing
attempts, any text (anywhere in the webclient) that switches unicode "blocks"
(a rough approximation to (language) scripts) within a word is underlined
orange. multiple messages can be selected with familiar ui interaction:
clicking while holding control and/or shift keys. keyboard navigation works
with arrows/page up/down and home/end keys, and also with a few basic vi-like
keys for list/message navigation. we prefer showing the text instead of
html (with inlined images only) version of a message. html messages are shown
in an iframe served from an endpoint with CSP headers to prevent dangerous
resources (scripts, external images) from being loaded. the html is also
sanitized, with javascript removed. a user can choose to load external
resources (e.g. images for tracking purposes).
the frontend is just (strict) typescript, no external frameworks. all
incoming/outgoing data is typechecked, both the api request parameters and
response types, and the data coming in over SSE. the types and checking code
are generated with sherpats, which uses the api definitions generated by
sherpadoc based on the Go code. so types from the backend are automatically
propagated to the frontend. since there is no framework to automatically
propagate properties and rerender components, changes coming in over the SSE
connection are propagated explicitly with regular function calls. the ui is
separated into "views", each with a "root" dom element that is added to the
visible document. these views have additional functions for getting changes
propagated, often resulting in the view updating its (internal) ui state (dom).
we keep the frontend compilation simple, it's just a few typescript files that
get compiled (combined and types stripped) into a single js file, no additional
runtime code needed or complicated build processes used. the webmail is served
is served from a compressed, cachable html file that includes style and the
javascript, currently just over 225kb uncompressed, under 60kb compressed (not
minified, including comments). we include the generated js files in the
repository, to keep Go's easily buildable self-contained binaries.
authentication is basic http, as with the account and admin pages. most data
comes in over one long-term SSE connection to the backend. api requests signal
which mailbox/search/messages are requested over the SSE connection. fetching
individual messages, and making changes, are done through api calls. the
operations are similar to imap, so some code has been moved from package
imapserver to package store. the future jmap implementation will benefit from
these changes too. more functionality will probably be moved to the store
package in the future.
the quickstart enables webmail on the internal listener by default (for new
installs). users can enable it on the public listener if they want to. mox
localserve enables it too. to enable webmail on existing installs, add settings
like the following to the listeners in mox.conf, similar to AccountHTTP(S):
WebmailHTTP:
Enabled: true
WebmailHTTPS:
Enabled: true
special thanks to liesbeth, gerben, andrii for early user feedback.
there is plenty still to do, see the list at the top of webmail/webmail.ts.
feedback welcome as always.
2023-08-07 22:57:03 +03:00
cmd . deltaCounts . Sub ( m . MailboxCounts ( ) )
origFlags := m . Flags
2023-01-30 16:27:06 +03:00
m . Seen = true
add webmail
it was far down on the roadmap, but implemented earlier, because it's
interesting, and to help prepare for a jmap implementation. for jmap we need to
implement more client-like functionality than with just imap. internal data
structures need to change. jmap has lots of other requirements, so it's already
a big project. by implementing a webmail now, some of the required data
structure changes become clear and can be made now, so the later jmap
implementation can do things similarly to the webmail code. the webmail
frontend and webmail are written together, making their interface/api much
smaller and simpler than jmap.
one of the internal changes is that we now keep track of per-mailbox
total/unread/unseen/deleted message counts and mailbox sizes. keeping this
data consistent after any change to the stored messages (through the code base)
is tricky, so mox now has a consistency check that verifies the counts are
correct, which runs only during tests, each time an internal account reference
is closed. we have a few more internal "changes" that are propagated for the
webmail frontend (that imap doesn't have a way to propagate on a connection),
like changes to the special-use flags on mailboxes, and used keywords in a
mailbox. more changes that will be required have revealed themselves while
implementing the webmail, and will be implemented next.
the webmail user interface is modeled after the mail clients i use or have
used: thunderbird, macos mail, mutt; and webmails i normally only use for
testing: gmail, proton, yahoo, outlook. a somewhat technical user is assumed,
but still the goal is to make this webmail client easy to use for everyone. the
user interface looks like most other mail clients: a list of mailboxes, a
search bar, a message list view, and message details. there is a top/bottom and
a left/right layout for the list/message view, default is automatic based on
screen size. the panes can be resized by the user. buttons for actions are just
text, not icons. clicking a button briefly shows the shortcut for the action in
the bottom right, helping with learning to operate quickly. any text that is
underdotted has a title attribute that causes more information to be displayed,
e.g. what a button does or a field is about. to highlight potential phishing
attempts, any text (anywhere in the webclient) that switches unicode "blocks"
(a rough approximation to (language) scripts) within a word is underlined
orange. multiple messages can be selected with familiar ui interaction:
clicking while holding control and/or shift keys. keyboard navigation works
with arrows/page up/down and home/end keys, and also with a few basic vi-like
keys for list/message navigation. we prefer showing the text instead of
html (with inlined images only) version of a message. html messages are shown
in an iframe served from an endpoint with CSP headers to prevent dangerous
resources (scripts, external images) from being loaded. the html is also
sanitized, with javascript removed. a user can choose to load external
resources (e.g. images for tracking purposes).
the frontend is just (strict) typescript, no external frameworks. all
incoming/outgoing data is typechecked, both the api request parameters and
response types, and the data coming in over SSE. the types and checking code
are generated with sherpats, which uses the api definitions generated by
sherpadoc based on the Go code. so types from the backend are automatically
propagated to the frontend. since there is no framework to automatically
propagate properties and rerender components, changes coming in over the SSE
connection are propagated explicitly with regular function calls. the ui is
separated into "views", each with a "root" dom element that is added to the
visible document. these views have additional functions for getting changes
propagated, often resulting in the view updating its (internal) ui state (dom).
we keep the frontend compilation simple, it's just a few typescript files that
get compiled (combined and types stripped) into a single js file, no additional
runtime code needed or complicated build processes used. the webmail is served
is served from a compressed, cachable html file that includes style and the
javascript, currently just over 225kb uncompressed, under 60kb compressed (not
minified, including comments). we include the generated js files in the
repository, to keep Go's easily buildable self-contained binaries.
authentication is basic http, as with the account and admin pages. most data
comes in over one long-term SSE connection to the backend. api requests signal
which mailbox/search/messages are requested over the SSE connection. fetching
individual messages, and making changes, are done through api calls. the
operations are similar to imap, so some code has been moved from package
imapserver to package store. the future jmap implementation will benefit from
these changes too. more functionality will probably be moved to the store
package in the future.
the quickstart enables webmail on the internal listener by default (for new
installs). users can enable it on the public listener if they want to. mox
localserve enables it too. to enable webmail on existing installs, add settings
like the following to the listeners in mox.conf, similar to AccountHTTP(S):
WebmailHTTP:
Enabled: true
WebmailHTTPS:
Enabled: true
special thanks to liesbeth, gerben, andrii for early user feedback.
there is plenty still to do, see the list at the top of webmail/webmail.ts.
feedback welcome as always.
2023-08-07 22:57:03 +03:00
cmd . deltaCounts . Add ( m . MailboxCounts ( ) )
2023-07-24 22:21:05 +03:00
m . ModSeq = cmd . xmodseq ( )
2023-01-30 16:27:06 +03:00
err := cmd . tx . Update ( m )
xcheckf ( err , "marking message as seen" )
2023-12-20 22:54:12 +03:00
// No need to update account total message size.
2023-01-30 16:27:06 +03:00
add webmail
it was far down on the roadmap, but implemented earlier, because it's
interesting, and to help prepare for a jmap implementation. for jmap we need to
implement more client-like functionality than with just imap. internal data
structures need to change. jmap has lots of other requirements, so it's already
a big project. by implementing a webmail now, some of the required data
structure changes become clear and can be made now, so the later jmap
implementation can do things similarly to the webmail code. the webmail
frontend and webmail are written together, making their interface/api much
smaller and simpler than jmap.
one of the internal changes is that we now keep track of per-mailbox
total/unread/unseen/deleted message counts and mailbox sizes. keeping this
data consistent after any change to the stored messages (through the code base)
is tricky, so mox now has a consistency check that verifies the counts are
correct, which runs only during tests, each time an internal account reference
is closed. we have a few more internal "changes" that are propagated for the
webmail frontend (that imap doesn't have a way to propagate on a connection),
like changes to the special-use flags on mailboxes, and used keywords in a
mailbox. more changes that will be required have revealed themselves while
implementing the webmail, and will be implemented next.
the webmail user interface is modeled after the mail clients i use or have
used: thunderbird, macos mail, mutt; and webmails i normally only use for
testing: gmail, proton, yahoo, outlook. a somewhat technical user is assumed,
but still the goal is to make this webmail client easy to use for everyone. the
user interface looks like most other mail clients: a list of mailboxes, a
search bar, a message list view, and message details. there is a top/bottom and
a left/right layout for the list/message view, default is automatic based on
screen size. the panes can be resized by the user. buttons for actions are just
text, not icons. clicking a button briefly shows the shortcut for the action in
the bottom right, helping with learning to operate quickly. any text that is
underdotted has a title attribute that causes more information to be displayed,
e.g. what a button does or a field is about. to highlight potential phishing
attempts, any text (anywhere in the webclient) that switches unicode "blocks"
(a rough approximation to (language) scripts) within a word is underlined
orange. multiple messages can be selected with familiar ui interaction:
clicking while holding control and/or shift keys. keyboard navigation works
with arrows/page up/down and home/end keys, and also with a few basic vi-like
keys for list/message navigation. we prefer showing the text instead of
html (with inlined images only) version of a message. html messages are shown
in an iframe served from an endpoint with CSP headers to prevent dangerous
resources (scripts, external images) from being loaded. the html is also
sanitized, with javascript removed. a user can choose to load external
resources (e.g. images for tracking purposes).
the frontend is just (strict) typescript, no external frameworks. all
incoming/outgoing data is typechecked, both the api request parameters and
response types, and the data coming in over SSE. the types and checking code
are generated with sherpats, which uses the api definitions generated by
sherpadoc based on the Go code. so types from the backend are automatically
propagated to the frontend. since there is no framework to automatically
propagate properties and rerender components, changes coming in over the SSE
connection are propagated explicitly with regular function calls. the ui is
separated into "views", each with a "root" dom element that is added to the
visible document. these views have additional functions for getting changes
propagated, often resulting in the view updating its (internal) ui state (dom).
we keep the frontend compilation simple, it's just a few typescript files that
get compiled (combined and types stripped) into a single js file, no additional
runtime code needed or complicated build processes used. the webmail is served
is served from a compressed, cachable html file that includes style and the
javascript, currently just over 225kb uncompressed, under 60kb compressed (not
minified, including comments). we include the generated js files in the
repository, to keep Go's easily buildable self-contained binaries.
authentication is basic http, as with the account and admin pages. most data
comes in over one long-term SSE connection to the backend. api requests signal
which mailbox/search/messages are requested over the SSE connection. fetching
individual messages, and making changes, are done through api calls. the
operations are similar to imap, so some code has been moved from package
imapserver to package store. the future jmap implementation will benefit from
these changes too. more functionality will probably be moved to the store
package in the future.
the quickstart enables webmail on the internal listener by default (for new
installs). users can enable it on the public listener if they want to. mox
localserve enables it too. to enable webmail on existing installs, add settings
like the following to the listeners in mox.conf, similar to AccountHTTP(S):
WebmailHTTP:
Enabled: true
WebmailHTTPS:
Enabled: true
special thanks to liesbeth, gerben, andrii for early user feedback.
there is plenty still to do, see the list at the top of webmail/webmail.ts.
feedback welcome as always.
2023-08-07 22:57:03 +03:00
cmd . changes = append ( cmd . changes , m . ChangeFlags ( origFlags ) )
2023-01-30 16:27:06 +03:00
}
if cmd . needFlags {
m := cmd . xensureMessage ( )
2023-06-24 01:24:43 +03:00
data = append ( data , bare ( "FLAGS" ) , flaglist ( m . Flags , m . Keywords ) )
2023-01-30 16:27:06 +03:00
}
2023-07-24 22:21:05 +03:00
// The wording around when to include the MODSEQ attribute is hard to follow and is
// specified and refined in several places.
//
// An additional rule applies to "QRESYNC servers" (we'll assume it only applies
// when QRESYNC is enabled on a connection): setting the \Seen flag also triggers
// sending MODSEQ, and so does a UID FETCH command. ../rfc/7162:1421
//
// For example, ../rfc/7162:389 says the server must include modseq in "all
// subsequent untagged fetch responses", then lists cases, but leaves out FETCH/UID
// FETCH. That appears intentional, it is not a list of examples, it is the full
// list, and the "all subsequent untagged fetch responses" doesn't mean "all", just
// those covering the listed cases. That makes sense, because otherwise all the
// other mentioning of cases elsewhere in the RFC would be too superfluous.
//
// ../rfc/7162:877 ../rfc/7162:388 ../rfc/7162:909 ../rfc/7162:1426
if cmd . needModseq || cmd . hasChangedSince || cmd . conn . enabled [ capQresync ] && ( cmd . isUID || cmd . markSeen ) {
m := cmd . xensureMessage ( )
data = append ( data , bare ( "MODSEQ" ) , listspace { bare ( fmt . Sprintf ( "%d" , m . ModSeq . Client ( ) ) ) } )
}
2023-01-30 16:27:06 +03:00
// Write errors are turned into panics because we write through c.
fmt . Fprintf ( cmd . conn . bw , "* %d FETCH " , cmd . conn . xsequence ( cmd . uid ) )
data . writeTo ( cmd . conn , cmd . conn . bw )
cmd . conn . bw . Write ( [ ] byte ( "\r\n" ) )
}
// result for one attribute. if processing fails, e.g. because data was requested
// that doesn't exist and cannot be represented in imap, the attribute is simply
// not returned to the user. in this case, the returned value is a nil list.
func ( cmd * fetchCmd ) xprocessAtt ( a fetchAtt ) [ ] token {
switch a . field {
case "UID" :
// Always present.
return nil
case "ENVELOPE" :
_ , part := cmd . xensureParsed ( )
envelope := xenvelope ( part )
return [ ] token { bare ( "ENVELOPE" ) , envelope }
case "INTERNALDATE" :
// ../rfc/9051:6753 ../rfc/9051:6502
m := cmd . xensureMessage ( )
return [ ] token { bare ( "INTERNALDATE" ) , dquote ( m . Received . Format ( "_2-Jan-2006 15:04:05 -0700" ) ) }
case "BODYSTRUCTURE" :
_ , part := cmd . xensureParsed ( )
2024-11-01 13:28:25 +03:00
bs := xbodystructure ( part , true )
2023-01-30 16:27:06 +03:00
return [ ] token { bare ( "BODYSTRUCTURE" ) , bs }
case "BODY" :
respField , t := cmd . xbody ( a )
if respField == "" {
return nil
}
return [ ] token { bare ( respField ) , t }
case "BINARY.SIZE" :
_ , p := cmd . xensureParsed ( )
if len ( a . sectionBinary ) == 0 {
// Must return the size of the entire message but with decoded body.
// todo: make this less expensive and/or cache the result?
n , err := io . Copy ( io . Discard , cmd . xbinaryMessageReader ( p ) )
cmd . xcheckf ( err , "reading message as binary for its size" )
return [ ] token { bare ( cmd . sectionRespField ( a ) ) , number ( uint32 ( n ) ) }
}
p = cmd . xpartnumsDeref ( a . sectionBinary , p )
if len ( p . Parts ) > 0 || p . Message != nil {
// ../rfc/9051:4385
cmd . xerrorf ( "binary only allowed on leaf parts, not multipart/* or message/rfc822 or message/global" )
}
return [ ] token { bare ( cmd . sectionRespField ( a ) ) , number ( p . DecodedSize ) }
case "BINARY" :
respField , t := cmd . xbinary ( a )
if respField == "" {
return nil
}
return [ ] token { bare ( respField ) , t }
case "RFC822.SIZE" :
m := cmd . xensureMessage ( )
return [ ] token { bare ( "RFC822.SIZE" ) , number ( m . Size ) }
case "RFC822.HEADER" :
ba := fetchAtt {
field : "BODY" ,
peek : true ,
section : & sectionSpec {
msgtext : & sectionMsgtext { s : "HEADER" } ,
} ,
}
respField , t := cmd . xbody ( ba )
if respField == "" {
return nil
}
return [ ] token { bare ( a . field ) , t }
case "RFC822" :
ba := fetchAtt {
field : "BODY" ,
section : & sectionSpec { } ,
}
respField , t := cmd . xbody ( ba )
if respField == "" {
return nil
}
return [ ] token { bare ( a . field ) , t }
case "RFC822.TEXT" :
ba := fetchAtt {
field : "BODY" ,
section : & sectionSpec {
msgtext : & sectionMsgtext { s : "TEXT" } ,
} ,
}
respField , t := cmd . xbody ( ba )
if respField == "" {
return nil
}
return [ ] token { bare ( a . field ) , t }
case "FLAGS" :
cmd . needFlags = true
2023-07-24 22:21:05 +03:00
case "MODSEQ" :
cmd . needModseq = true
2023-01-30 16:27:06 +03:00
default :
xserverErrorf ( "field %q not yet implemented" , a . field )
}
return nil
}
// ../rfc/9051:6522
func xenvelope ( p * message . Part ) token {
var env message . Envelope
if p . Envelope != nil {
env = * p . Envelope
}
var date token = nilt
if ! env . Date . IsZero ( ) {
// ../rfc/5322:791
date = string0 ( env . Date . Format ( "Mon, 2 Jan 2006 15:04:05 -0700" ) )
}
var subject token = nilt
if env . Subject != "" {
subject = string0 ( env . Subject )
}
var inReplyTo token = nilt
if env . InReplyTo != "" {
inReplyTo = string0 ( env . InReplyTo )
}
var messageID token = nilt
if env . MessageID != "" {
messageID = string0 ( env . MessageID )
}
addresses := func ( l [ ] message . Address ) token {
if len ( l ) == 0 {
return nilt
}
r := listspace { }
for _ , a := range l {
var name token = nilt
if a . Name != "" {
name = string0 ( a . Name )
}
user := string0 ( a . User )
var host token = nilt
if a . Host != "" {
host = string0 ( a . Host )
}
r = append ( r , listspace { name , nilt , user , host } )
}
return r
}
// Empty sender or reply-to result in fall-back to from. ../rfc/9051:6140
sender := env . Sender
if len ( sender ) == 0 {
sender = env . From
}
replyTo := env . ReplyTo
if len ( replyTo ) == 0 {
replyTo = env . From
}
return listspace {
date ,
subject ,
addresses ( env . From ) ,
addresses ( sender ) ,
addresses ( replyTo ) ,
addresses ( env . To ) ,
addresses ( env . CC ) ,
addresses ( env . BCC ) ,
inReplyTo ,
messageID ,
}
}
func ( cmd * fetchCmd ) peekOrSeen ( peek bool ) {
if cmd . conn . readonly || peek {
return
}
m := cmd . xensureMessage ( )
if ! m . Seen {
cmd . markSeen = true
cmd . needFlags = true
}
}
// reader that returns the message, but with header Content-Transfer-Encoding left out.
func ( cmd * fetchCmd ) xbinaryMessageReader ( p * message . Part ) io . Reader {
hr := cmd . xmodifiedHeader ( p , [ ] string { "Content-Transfer-Encoding" } , true )
return io . MultiReader ( hr , p . Reader ( ) )
}
// return header with only fields, or with everything except fields if "not" is set.
func ( cmd * fetchCmd ) xmodifiedHeader ( p * message . Part , fields [ ] string , not bool ) io . Reader {
h , err := io . ReadAll ( p . HeaderReader ( ) )
cmd . xcheckf ( err , "reading header" )
matchesFields := func ( line [ ] byte ) bool {
k := bytes . TrimRight ( bytes . SplitN ( line , [ ] byte ( ":" ) , 2 ) [ 0 ] , " \t" )
for _ , f := range fields {
if bytes . EqualFold ( k , [ ] byte ( f ) ) {
return true
}
}
return false
}
var match bool
hb := & bytes . Buffer { }
for len ( h ) > 0 {
line := h
i := bytes . Index ( line , [ ] byte ( "\r\n" ) )
if i >= 0 {
line = line [ : i + 2 ]
}
h = h [ len ( line ) : ]
match = matchesFields ( line ) || match && ( bytes . HasPrefix ( line , [ ] byte ( " " ) ) || bytes . HasPrefix ( line , [ ] byte ( "\t" ) ) )
if match != not || len ( line ) == 2 {
hb . Write ( line )
}
}
return hb
}
func ( cmd * fetchCmd ) xbinary ( a fetchAtt ) ( string , token ) {
_ , part := cmd . xensureParsed ( )
cmd . peekOrSeen ( a . peek )
if len ( a . sectionBinary ) == 0 {
r := cmd . xbinaryMessageReader ( part )
if a . partial != nil {
r = cmd . xpartialReader ( a . partial , r )
}
return cmd . sectionRespField ( a ) , readerSyncliteral { r }
}
p := part
if len ( a . sectionBinary ) > 0 {
p = cmd . xpartnumsDeref ( a . sectionBinary , p )
}
if len ( p . Parts ) != 0 || p . Message != nil {
// ../rfc/9051:4385
cmd . xerrorf ( "binary only allowed on leaf parts, not multipart/* or message/rfc822 or message/global" )
}
switch p . ContentTransferEncoding {
case "" , "7BIT" , "8BIT" , "BINARY" , "BASE64" , "QUOTED-PRINTABLE" :
default :
// ../rfc/9051:5913
xusercodeErrorf ( "UNKNOWN-CTE" , "unknown Content-Transfer-Encoding %q" , p . ContentTransferEncoding )
}
r := p . Reader ( )
if a . partial != nil {
r = cmd . xpartialReader ( a . partial , r )
}
return cmd . sectionRespField ( a ) , readerSyncliteral { r }
}
func ( cmd * fetchCmd ) xpartialReader ( partial * partial , r io . Reader ) io . Reader {
n , err := io . Copy ( io . Discard , io . LimitReader ( r , int64 ( partial . offset ) ) )
cmd . xcheckf ( err , "skipping to offset for partial" )
if n != int64 ( partial . offset ) {
return strings . NewReader ( "" ) // ../rfc/3501:3143 ../rfc/9051:4418
}
return io . LimitReader ( r , int64 ( partial . count ) )
}
func ( cmd * fetchCmd ) xbody ( a fetchAtt ) ( string , token ) {
msgr , part := cmd . xensureParsed ( )
if a . section == nil {
// Non-extensible form of BODYSTRUCTURE.
2024-11-01 13:28:25 +03:00
return a . field , xbodystructure ( part , false )
2023-01-30 16:27:06 +03:00
}
cmd . peekOrSeen ( a . peek )
respField := cmd . sectionRespField ( a )
if a . section . msgtext == nil && a . section . part == nil {
m := cmd . xensureMessage ( )
var offset int64
count := m . Size
if a . partial != nil {
offset = int64 ( a . partial . offset )
if offset > m . Size {
offset = m . Size
}
count = int64 ( a . partial . count )
if offset + count > m . Size {
count = m . Size - offset
}
}
return respField , readerSizeSyncliteral { & moxio . AtReader { R : msgr , Offset : offset } , count }
}
sr := cmd . xsection ( a . section , part )
if a . partial != nil {
n , err := io . Copy ( io . Discard , io . LimitReader ( sr , int64 ( a . partial . offset ) ) )
cmd . xcheckf ( err , "skipping to offset for partial" )
if n != int64 ( a . partial . offset ) {
return respField , syncliteral ( "" ) // ../rfc/3501:3143 ../rfc/9051:4418
}
return respField , readerSyncliteral { io . LimitReader ( sr , int64 ( a . partial . count ) ) }
}
return respField , readerSyncliteral { sr }
}
func ( cmd * fetchCmd ) xpartnumsDeref ( nums [ ] uint32 , p * message . Part ) * message . Part {
// ../rfc/9051:4481
if ( len ( p . Parts ) == 0 && p . Message == nil ) && len ( nums ) == 1 && nums [ 0 ] == 1 {
return p
}
// ../rfc/9051:4485
for i , num := range nums {
index := int ( num - 1 )
if p . Message != nil {
err := p . SetMessageReaderAt ( )
cmd . xcheckf ( err , "preparing submessage" )
return cmd . xpartnumsDeref ( nums [ i : ] , p . Message )
}
if index < 0 || index >= len ( p . Parts ) {
cmd . xerrorf ( "requested part does not exist" )
}
p = & p . Parts [ index ]
}
return p
}
func ( cmd * fetchCmd ) xsection ( section * sectionSpec , p * message . Part ) io . Reader {
if section . part == nil {
return cmd . xsectionMsgtext ( section . msgtext , p )
}
p = cmd . xpartnumsDeref ( section . part . part , p )
if section . part . text == nil {
return p . RawReader ( )
}
// ../rfc/9051:4535
if p . Message != nil {
err := p . SetMessageReaderAt ( )
cmd . xcheckf ( err , "preparing submessage" )
p = p . Message
}
if ! section . part . text . mime {
return cmd . xsectionMsgtext ( section . part . text . msgtext , p )
}
// MIME header, see ../rfc/9051:4534 ../rfc/2045:1645
h , err := io . ReadAll ( p . HeaderReader ( ) )
cmd . xcheckf ( err , "reading header" )
matchesFields := func ( line [ ] byte ) bool {
k := textproto . CanonicalMIMEHeaderKey ( string ( bytes . TrimRight ( bytes . SplitN ( line , [ ] byte ( ":" ) , 2 ) [ 0 ] , " \t" ) ) )
// Only add MIME-Version and additional CRLF for messages, not other parts. ../rfc/2045:1645 ../rfc/2045:1652
return ( p . Envelope != nil && k == "Mime-Version" ) || strings . HasPrefix ( k , "Content-" )
}
var match bool
hb := & bytes . Buffer { }
for len ( h ) > 0 {
line := h
i := bytes . Index ( line , [ ] byte ( "\r\n" ) )
if i >= 0 {
line = line [ : i + 2 ]
}
h = h [ len ( line ) : ]
match = matchesFields ( line ) || match && ( bytes . HasPrefix ( line , [ ] byte ( " " ) ) || bytes . HasPrefix ( line , [ ] byte ( "\t" ) ) )
if match || len ( line ) == 2 {
hb . Write ( line )
}
}
return hb
}
func ( cmd * fetchCmd ) xsectionMsgtext ( smt * sectionMsgtext , p * message . Part ) io . Reader {
if smt . s == "HEADER" {
return p . HeaderReader ( )
}
switch smt . s {
case "HEADER.FIELDS" :
return cmd . xmodifiedHeader ( p , smt . headers , false )
case "HEADER.FIELDS.NOT" :
return cmd . xmodifiedHeader ( p , smt . headers , true )
case "TEXT" :
// It appears imap clients expect to get the body of the message, not a "text body"
// which sounds like it means a text/* part of a message. ../rfc/9051:4517
return p . RawReader ( )
}
panic ( serverError { fmt . Errorf ( "missing case" ) } )
}
func ( cmd * fetchCmd ) sectionRespField ( a fetchAtt ) string {
s := a . field + "["
if len ( a . sectionBinary ) > 0 {
s += fmt . Sprintf ( "%d" , a . sectionBinary [ 0 ] )
for _ , v := range a . sectionBinary [ 1 : ] {
s += "." + fmt . Sprintf ( "%d" , v )
}
} else if a . section != nil {
if a . section . part != nil {
p := a . section . part
s += fmt . Sprintf ( "%d" , p . part [ 0 ] )
for _ , v := range p . part [ 1 : ] {
s += "." + fmt . Sprintf ( "%d" , v )
}
if p . text != nil {
if p . text . mime {
s += ".MIME"
} else {
s += "." + cmd . sectionMsgtextName ( p . text . msgtext )
}
}
} else if a . section . msgtext != nil {
s += cmd . sectionMsgtextName ( a . section . msgtext )
}
}
s += "]"
// binary does not have partial in field, unlike BODY ../rfc/9051:6757
if a . field != "BINARY" && a . partial != nil {
s += fmt . Sprintf ( "<%d>" , a . partial . offset )
}
return s
}
func ( cmd * fetchCmd ) sectionMsgtextName ( smt * sectionMsgtext ) string {
s := smt . s
if strings . HasPrefix ( smt . s , "HEADER.FIELDS" ) {
l := listspace { }
for _ , h := range smt . headers {
l = append ( l , astring ( h ) )
}
s += " " + l . pack ( cmd . conn )
}
return s
}
func bodyFldParams ( params map [ string ] string ) token {
if len ( params ) == 0 {
return nilt
}
// Ensure same ordering, easier for testing.
var keys [ ] string
for k := range params {
keys = append ( keys , k )
}
sort . Strings ( keys )
l := make ( listspace , 2 * len ( keys ) )
i := 0
for _ , k := range keys {
l [ i ] = string0 ( strings . ToUpper ( k ) )
l [ i + 1 ] = string0 ( params [ k ] )
i += 2
}
return l
}
func bodyFldEnc ( s string ) token {
up := strings . ToUpper ( s )
switch up {
case "7BIT" , "8BIT" , "BINARY" , "BASE64" , "QUOTED-PRINTABLE" :
return dquote ( up )
}
return string0 ( s )
}
// xbodystructure returns a "body".
// calls itself for multipart messages and message/{rfc822,global}.
2024-11-01 13:28:25 +03:00
func xbodystructure ( p * message . Part , extensible bool ) token {
2023-01-30 16:27:06 +03:00
if p . MediaType == "MULTIPART" {
// Multipart, ../rfc/9051:6355 ../rfc/9051:6411
var bodies concat
for i := range p . Parts {
2024-11-01 13:28:25 +03:00
bodies = append ( bodies , xbodystructure ( & p . Parts [ i ] , extensible ) )
2023-01-30 16:27:06 +03:00
}
2024-11-01 13:28:25 +03:00
r := listspace { bodies , string0 ( p . MediaSubType ) }
if extensible {
if len ( p . ContentTypeParams ) == 0 {
r = append ( r , nilt )
} else {
params := make ( listspace , 0 , 2 * len ( p . ContentTypeParams ) )
for k , v := range p . ContentTypeParams {
params = append ( params , string0 ( k ) , string0 ( v ) )
}
r = append ( r , params )
}
}
return r
2023-01-30 16:27:06 +03:00
}
// ../rfc/9051:6355
2024-11-01 13:28:25 +03:00
var r listspace
2023-01-30 16:27:06 +03:00
if p . MediaType == "TEXT" {
// ../rfc/9051:6404 ../rfc/9051:6418
2024-11-01 13:28:25 +03:00
r = listspace {
2023-01-30 16:27:06 +03:00
dquote ( "TEXT" ) , string0 ( p . MediaSubType ) , // ../rfc/9051:6739
// ../rfc/9051:6376
bodyFldParams ( p . ContentTypeParams ) , // ../rfc/9051:6401
nilOrString ( p . ContentID ) ,
nilOrString ( p . ContentDescription ) ,
bodyFldEnc ( p . ContentTransferEncoding ) ,
number ( p . EndOffset - p . BodyOffset ) ,
number ( p . RawLineCount ) ,
}
} else if p . MediaType == "MESSAGE" && ( p . MediaSubType == "RFC822" || p . MediaSubType == "GLOBAL" ) {
// ../rfc/9051:6415
// note: we don't have to prepare p.Message for reading, because we aren't going to read from it.
2024-11-01 13:28:25 +03:00
r = listspace {
2023-01-30 16:27:06 +03:00
dquote ( "MESSAGE" ) , dquote ( p . MediaSubType ) , // ../rfc/9051:6732
// ../rfc/9051:6376
bodyFldParams ( p . ContentTypeParams ) , // ../rfc/9051:6401
nilOrString ( p . ContentID ) ,
nilOrString ( p . ContentDescription ) ,
bodyFldEnc ( p . ContentTransferEncoding ) ,
number ( p . EndOffset - p . BodyOffset ) ,
xenvelope ( p . Message ) ,
2024-11-01 13:28:25 +03:00
xbodystructure ( p . Message , extensible ) ,
2023-01-30 16:27:06 +03:00
number ( p . RawLineCount ) , // todo: or mp.RawLineCount?
}
2024-11-01 13:28:25 +03:00
} else {
var media token
switch p . MediaType {
case "APPLICATION" , "AUDIO" , "IMAGE" , "FONT" , "MESSAGE" , "MODEL" , "VIDEO" :
media = dquote ( p . MediaType )
default :
media = string0 ( p . MediaType )
}
// ../rfc/9051:6404 ../rfc/9051:6407
r = listspace {
media , string0 ( p . MediaSubType ) , // ../rfc/9051:6723
// ../rfc/9051:6376
bodyFldParams ( p . ContentTypeParams ) , // ../rfc/9051:6401
nilOrString ( p . ContentID ) ,
nilOrString ( p . ContentDescription ) ,
bodyFldEnc ( p . ContentTransferEncoding ) ,
number ( p . EndOffset - p . BodyOffset ) ,
}
2023-01-30 16:27:06 +03:00
}
2024-11-01 13:28:25 +03:00
// todo: if "extensible", we could add the value of the "content-md5" header. we don't have it in our parsed data structure, so we don't add it. likely no one would use it, also not any of the other optional fields. ../rfc/9051:6366
return r
2023-01-30 16:27:06 +03:00
}