mirror of
https://github.com/mjl-/mox.git
synced 2024-12-27 08:53:48 +03:00
Merge branch 'main' into tls-alpn-mux
This commit is contained in:
commit
2db7323921
31 changed files with 493 additions and 370 deletions
30
apidiff.sh
30
apidiff.sh
|
@ -8,20 +8,30 @@ if ! test -d tmp/mox-$prevversion; then
|
|||
fi
|
||||
(rm -r tmp/apidiff || exit 0)
|
||||
mkdir -p tmp/apidiff/$prevversion tmp/apidiff/next
|
||||
(rm apidiff/next.txt || exit 0)
|
||||
(
|
||||
echo "Below are the incompatible changes between $prevversion and next, per package."
|
||||
echo
|
||||
) >>apidiff/next.txt
|
||||
(rm apidiff/next.txt apidiff/next.txt.new 2>/dev/null || exit 0)
|
||||
for p in $(cat apidiff/packages.txt); do
|
||||
if ! test -d tmp/mox-$prevversion/$p; then
|
||||
continue
|
||||
fi
|
||||
(cd tmp/mox-$prevversion && apidiff -w ../apidiff/$prevversion/$p.api ./$p)
|
||||
apidiff -w tmp/apidiff/next/$p.api ./$p
|
||||
(
|
||||
echo '#' $p
|
||||
apidiff -incompatible tmp/apidiff/$prevversion/$p.api tmp/apidiff/next/$p.api
|
||||
echo
|
||||
) >>apidiff/next.txt
|
||||
apidiff -incompatible tmp/apidiff/$prevversion/$p.api tmp/apidiff/next/$p.api >$p.diff
|
||||
if test -s $p.diff; then
|
||||
(
|
||||
echo '#' $p
|
||||
cat $p.diff
|
||||
echo
|
||||
) >>apidiff/next.txt.new
|
||||
fi
|
||||
rm $p.diff
|
||||
done
|
||||
if test -s apidiff/next.txt.new; then
|
||||
(
|
||||
echo "Below are the incompatible changes between $prevversion and next, per package."
|
||||
echo
|
||||
cat apidiff/next.txt.new
|
||||
) >apidiff/next.txt
|
||||
rm apidiff/next.txt.new
|
||||
else
|
||||
mv apidiff/next.txt.new apidiff/next.txt
|
||||
fi
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
Below are the incompatible changes between v0.0.13 and next, per package.
|
||||
|
||||
# webhook
|
||||
- PartStructure: removed
|
||||
|
|
@ -312,12 +312,12 @@ func (m *Manager) SetAllowedHostnames(log mlog.Log, resolver dns.Resolver, hostn
|
|||
for _, h := range added {
|
||||
ips, _, err := resolver.LookupIP(ctx, "ip", h.ASCII+".")
|
||||
if err != nil {
|
||||
log.Errorx("warning: acme tls cert validation for host may fail due to dns lookup error", err, slog.Any("host", h))
|
||||
log.Warnx("acme tls cert validation for host may fail due to dns lookup error", err, slog.Any("host", h))
|
||||
continue
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if _, ok := publicIPstrs[ip.String()]; !ok {
|
||||
log.Error("warning: acme tls cert validation for host is likely to fail because not all its ips are being listened on",
|
||||
log.Warn("acme tls cert validation for host is likely to fail because not all its ips are being listened on",
|
||||
slog.Any("hostname", h),
|
||||
slog.Any("listenedips", publicIPs),
|
||||
slog.Any("hostips", ips),
|
||||
|
|
113
ctl.go
113
ctl.go
|
@ -1335,65 +1335,78 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
|||
case "retrain":
|
||||
/* protocol:
|
||||
> "retrain"
|
||||
> account
|
||||
> account or empty
|
||||
< "ok" or error
|
||||
*/
|
||||
account := ctl.xread()
|
||||
acc, err := store.OpenAccount(log, account)
|
||||
ctl.xcheck(err, "open account")
|
||||
defer func() {
|
||||
if acc != nil {
|
||||
err := acc.Close()
|
||||
log.Check(err, "closing account after retraining")
|
||||
}
|
||||
}()
|
||||
|
||||
acc.WithWLock(func() {
|
||||
conf, _ := acc.Conf()
|
||||
if conf.JunkFilter == nil {
|
||||
ctl.xcheck(store.ErrNoJunkFilter, "looking for junk filter")
|
||||
}
|
||||
|
||||
// Remove existing junk filter files.
|
||||
basePath := mox.DataDirPath("accounts")
|
||||
dbPath := filepath.Join(basePath, acc.Name, "junkfilter.db")
|
||||
bloomPath := filepath.Join(basePath, acc.Name, "junkfilter.bloom")
|
||||
err := os.Remove(dbPath)
|
||||
log.Check(err, "removing old junkfilter database file", slog.String("path", dbPath))
|
||||
err = os.Remove(bloomPath)
|
||||
log.Check(err, "removing old junkfilter bloom filter file", slog.String("path", bloomPath))
|
||||
|
||||
// Open junk filter, this creates new files.
|
||||
jf, _, err := acc.OpenJunkFilter(ctx, log)
|
||||
ctl.xcheck(err, "open new junk filter")
|
||||
xretrain := func(name string) {
|
||||
acc, err := store.OpenAccount(log, name)
|
||||
ctl.xcheck(err, "open account")
|
||||
defer func() {
|
||||
if jf == nil {
|
||||
return
|
||||
if acc != nil {
|
||||
err := acc.Close()
|
||||
log.Check(err, "closing account after retraining")
|
||||
}
|
||||
err := jf.Close()
|
||||
log.Check(err, "closing junk filter during cleanup")
|
||||
}()
|
||||
|
||||
// Read through messages with junk or nonjunk flag set, and train them.
|
||||
var total, trained int
|
||||
q := bstore.QueryDB[store.Message](ctx, acc.DB)
|
||||
q.FilterEqual("Expunged", false)
|
||||
err = q.ForEach(func(m store.Message) error {
|
||||
total++
|
||||
ok, err := acc.TrainMessage(ctx, log, jf, m)
|
||||
if ok {
|
||||
trained++
|
||||
}
|
||||
return err
|
||||
})
|
||||
ctl.xcheck(err, "training messages")
|
||||
log.Info("retrained messages", slog.Int("total", total), slog.Int("trained", trained))
|
||||
// todo: can we retrain an account without holding a write lock? perhaps by writing a junkfilter to a new location, and staying informed of message changes while we go through all messages in the account?
|
||||
|
||||
// Close junk filter, marking success.
|
||||
err = jf.Close()
|
||||
jf = nil
|
||||
ctl.xcheck(err, "closing junk filter")
|
||||
})
|
||||
acc.WithWLock(func() {
|
||||
conf, _ := acc.Conf()
|
||||
if conf.JunkFilter == nil {
|
||||
ctl.xcheck(store.ErrNoJunkFilter, "looking for junk filter")
|
||||
}
|
||||
|
||||
// Remove existing junk filter files.
|
||||
basePath := mox.DataDirPath("accounts")
|
||||
dbPath := filepath.Join(basePath, acc.Name, "junkfilter.db")
|
||||
bloomPath := filepath.Join(basePath, acc.Name, "junkfilter.bloom")
|
||||
err := os.Remove(dbPath)
|
||||
log.Check(err, "removing old junkfilter database file", slog.String("path", dbPath))
|
||||
err = os.Remove(bloomPath)
|
||||
log.Check(err, "removing old junkfilter bloom filter file", slog.String("path", bloomPath))
|
||||
|
||||
// Open junk filter, this creates new files.
|
||||
jf, _, err := acc.OpenJunkFilter(ctx, log)
|
||||
ctl.xcheck(err, "open new junk filter")
|
||||
defer func() {
|
||||
if jf == nil {
|
||||
return
|
||||
}
|
||||
err := jf.Close()
|
||||
log.Check(err, "closing junk filter during cleanup")
|
||||
}()
|
||||
|
||||
// Read through messages with junk or nonjunk flag set, and train them.
|
||||
var total, trained int
|
||||
q := bstore.QueryDB[store.Message](ctx, acc.DB)
|
||||
q.FilterEqual("Expunged", false)
|
||||
err = q.ForEach(func(m store.Message) error {
|
||||
total++
|
||||
ok, err := acc.TrainMessage(ctx, log, jf, m)
|
||||
if ok {
|
||||
trained++
|
||||
}
|
||||
return err
|
||||
})
|
||||
ctl.xcheck(err, "training messages")
|
||||
log.Info("retrained messages", slog.Int("total", total), slog.Int("trained", trained))
|
||||
|
||||
// Close junk filter, marking success.
|
||||
err = jf.Close()
|
||||
jf = nil
|
||||
ctl.xcheck(err, "closing junk filter")
|
||||
})
|
||||
}
|
||||
|
||||
if account == "" {
|
||||
for _, name := range mox.Conf.Accounts() {
|
||||
xretrain(name)
|
||||
}
|
||||
} else {
|
||||
xretrain(account)
|
||||
}
|
||||
ctl.xwriteok()
|
||||
|
||||
case "recalculatemailboxcounts":
|
||||
|
|
|
@ -117,7 +117,7 @@ func (s *Sig) Header() (string, error) {
|
|||
} else if i == len(s.SignedHeaders)-1 {
|
||||
v += ";"
|
||||
}
|
||||
w.Addf(sep, v)
|
||||
w.Addf(sep, "%s", v)
|
||||
}
|
||||
}
|
||||
if len(s.CopiedHeaders) > 0 {
|
||||
|
@ -139,7 +139,7 @@ func (s *Sig) Header() (string, error) {
|
|||
} else if i == len(s.CopiedHeaders)-1 {
|
||||
v += ";"
|
||||
}
|
||||
w.Addf(sep, v)
|
||||
w.Addf(sep, "%s", v)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
26
doc.go
26
doc.go
|
@ -105,7 +105,7 @@ any parameters. Followed by the help and usage information for each command.
|
|||
mox dnsbl check zone ip
|
||||
mox dnsbl checkhealth zone
|
||||
mox mtasts lookup domain
|
||||
mox retrain accountname
|
||||
mox retrain [accountname]
|
||||
mox sendmail [-Fname] [ignoredflags] [-t] [<message]
|
||||
mox spf check domain ip
|
||||
mox spf lookup domain
|
||||
|
@ -146,6 +146,8 @@ Quickstart writes configuration files, prints initial admin and account
|
|||
passwords, DNS records you should create. If you run it on Linux it writes a
|
||||
systemd service file and prints commands to enable and start mox as service.
|
||||
|
||||
All output is written to quickstart.log for later reference.
|
||||
|
||||
The user or uid is optional, defaults to "mox", and is the user or uid/gid mox
|
||||
will run as after initialization.
|
||||
|
||||
|
@ -1056,25 +1058,29 @@ error too, for reference.
|
|||
|
||||
# mox config alias list
|
||||
|
||||
List aliases for domain.
|
||||
Show aliases (lists) for domain.
|
||||
|
||||
usage: mox config alias list domain
|
||||
|
||||
# mox config alias print
|
||||
|
||||
Print settings and members of alias.
|
||||
Print settings and members of alias (list).
|
||||
|
||||
usage: mox config alias print alias
|
||||
|
||||
# mox config alias add
|
||||
|
||||
Add new alias with one or more addresses and public posting enabled.
|
||||
Add new alias (list) with one or more addresses and public posting enabled.
|
||||
|
||||
An alias is used for delivering incoming email to multiple recipients. If you
|
||||
want to add an address to an account, don't use an alias, just add the address
|
||||
to the account.
|
||||
|
||||
usage: mox config alias add alias@domain rcpt1@domain ...
|
||||
|
||||
# mox config alias update
|
||||
|
||||
Update alias configuration.
|
||||
Update alias (list) configuration.
|
||||
|
||||
usage: mox config alias update alias@domain [-postpublic false|true -listmembers false|true -allowmsgfrom false|true]
|
||||
-allowmsgfrom string
|
||||
|
@ -1086,19 +1092,19 @@ Update alias configuration.
|
|||
|
||||
# mox config alias rm
|
||||
|
||||
Remove alias.
|
||||
Remove alias (list).
|
||||
|
||||
usage: mox config alias rm alias@domain
|
||||
|
||||
# mox config alias addaddr
|
||||
|
||||
Add addresses to alias.
|
||||
Add addresses to alias (list).
|
||||
|
||||
usage: mox config alias addaddr alias@domain rcpt1@domain ...
|
||||
|
||||
# mox config alias rmaddr
|
||||
|
||||
Remove addresses from alias.
|
||||
Remove addresses from alias (list).
|
||||
|
||||
usage: mox config alias rmaddr alias@domain rcpt1@domain ...
|
||||
|
||||
|
@ -1390,12 +1396,12 @@ should be used, and how long the policy can be cached.
|
|||
|
||||
# mox retrain
|
||||
|
||||
Recreate and retrain the junk filter for the account.
|
||||
Recreate and retrain the junk filter for the account or all accounts.
|
||||
|
||||
Useful after having made changes to the junk filter configuration, or if the
|
||||
implementation has changed.
|
||||
|
||||
usage: mox retrain accountname
|
||||
usage: mox retrain [accountname]
|
||||
|
||||
# mox sendmail
|
||||
|
||||
|
|
44
http/web.go
44
http/web.go
|
@ -834,32 +834,38 @@ func portServes(l config.Listener) map[int]*serve {
|
|||
}
|
||||
|
||||
if l.TLS != nil && l.TLS.ACME != "" {
|
||||
hosts := map[dns.Domain]struct{}{
|
||||
mox.Conf.Static.HostnameDomain: {},
|
||||
m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
|
||||
if ensureManagerHosts[m] == nil {
|
||||
ensureManagerHosts[m] = map[dns.Domain]struct{}{}
|
||||
}
|
||||
hosts := ensureManagerHosts[m]
|
||||
hosts[mox.Conf.Static.HostnameDomain] = struct{}{}
|
||||
|
||||
if l.HostnameDomain.ASCII != "" {
|
||||
hosts[l.HostnameDomain] = struct{}{}
|
||||
}
|
||||
// All domains are served on all listeners. Gather autoconfig hostnames to ensure
|
||||
// presence of TLS certificates for.
|
||||
for _, name := range mox.Conf.Domains() {
|
||||
if dom, err := dns.ParseDomain(name); err != nil {
|
||||
pkglog.Errorx("parsing domain from config", err)
|
||||
} else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly {
|
||||
// Do not gather autoconfig name if we aren't accepting email for this domain.
|
||||
continue
|
||||
}
|
||||
|
||||
autoconfdom, err := dns.ParseDomain("autoconfig." + name)
|
||||
if err != nil {
|
||||
pkglog.Errorx("parsing domain from config for autoconfig", err)
|
||||
} else {
|
||||
hosts[autoconfdom] = struct{}{}
|
||||
// All domains are served on all listeners. Gather autoconfig hostnames to ensure
|
||||
// presence of TLS certificates. Fetching a certificate on-demand may be too slow
|
||||
// for the timeouts of clients doing autoconfig.
|
||||
|
||||
if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
|
||||
for _, name := range mox.Conf.Domains() {
|
||||
if dom, err := dns.ParseDomain(name); err != nil {
|
||||
pkglog.Errorx("parsing domain from config", err)
|
||||
} else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly {
|
||||
// Do not gather autoconfig name if we aren't accepting email for this domain.
|
||||
continue
|
||||
}
|
||||
|
||||
autoconfdom, err := dns.ParseDomain("autoconfig." + name)
|
||||
if err != nil {
|
||||
pkglog.Errorx("parsing domain from config for autoconfig", err)
|
||||
} else {
|
||||
hosts[autoconfdom] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
|
||||
ensureManagerHosts[m] = hosts
|
||||
}
|
||||
|
||||
for _, srv := range portServe {
|
||||
|
|
|
@ -45,28 +45,28 @@ func testSelectExamine(t *testing.T, examine bool) {
|
|||
uuidnext2 := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDNEXT", CodeArg: imapclient.CodeUint{Code: "UIDNEXT", Num: 2}, More: "x"}}
|
||||
|
||||
// Parameter required.
|
||||
tc.transactf("bad", cmd)
|
||||
tc.transactf("bad", "%s", cmd)
|
||||
|
||||
// Mailbox does not exist.
|
||||
tc.transactf("no", cmd+" bogus")
|
||||
tc.transactf("no", "%s bogus", cmd)
|
||||
|
||||
tc.transactf("ok", cmd+" inbox")
|
||||
tc.transactf("ok", "%s inbox", cmd)
|
||||
tc.xuntagged(uflags, upermflags, urecent, uexists0, uuidval1, uuidnext1, ulist)
|
||||
tc.xcode(okcode)
|
||||
|
||||
tc.transactf("ok", cmd+` "inbox"`)
|
||||
tc.transactf("ok", `%s "inbox"`, cmd)
|
||||
tc.xuntagged(uclosed, uflags, upermflags, urecent, uexists0, uuidval1, uuidnext1, ulist)
|
||||
tc.xcode(okcode)
|
||||
|
||||
// Append a message. It will be reported as UNSEEN.
|
||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
||||
tc.transactf("ok", cmd+" inbox")
|
||||
tc.transactf("ok", "%s inbox", cmd)
|
||||
tc.xuntagged(uclosed, uflags, upermflags, urecent, uunseen, uexists1, uuidval1, uuidnext2, ulist)
|
||||
tc.xcode(okcode)
|
||||
|
||||
// With imap4rev2, we no longer get untagged RECENT or untagged UNSEEN.
|
||||
tc.client.Enable("imap4rev2")
|
||||
tc.transactf("ok", cmd+" inbox")
|
||||
tc.transactf("ok", "%s inbox", cmd)
|
||||
tc.xuntagged(uclosed, uflags, upermflags, uexists1, uuidval1, uuidnext2, ulist)
|
||||
tc.xcode(okcode)
|
||||
}
|
||||
|
|
|
@ -1180,7 +1180,7 @@ func (c *conn) xsequence(uid store.UID) msgseq {
|
|||
func (c *conn) sequenceRemove(seq msgseq, uid store.UID) {
|
||||
i := seq - 1
|
||||
if c.uids[i] != uid {
|
||||
xserverErrorf(fmt.Sprintf("got uid %d at msgseq %d, expected uid %d", uid, seq, c.uids[i]))
|
||||
xserverErrorf("got uid %d at msgseq %d, expected uid %d", uid, seq, c.uids[i])
|
||||
}
|
||||
copy(c.uids[i:], c.uids[i+1:])
|
||||
c.uids = c.uids[:len(c.uids)-1]
|
||||
|
|
|
@ -298,7 +298,7 @@ func (f *Filter) Save() error {
|
|||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Update(&wordscore{w, wc.Ham + ham, wc.Spam + spam})
|
||||
return tx.Update(&wordscore{w, ham, spam})
|
||||
}
|
||||
if err := update("-", f.hams, f.spams); err != nil {
|
||||
return fmt.Errorf("storing total ham/spam message count: %s", err)
|
||||
|
@ -621,10 +621,16 @@ func (f *Filter) Untrain(ctx context.Context, ham bool, words map[string]struct{
|
|||
|
||||
// Modify the message count.
|
||||
f.modified = true
|
||||
var fv *uint32
|
||||
if ham {
|
||||
f.hams--
|
||||
fv = &f.hams
|
||||
} else {
|
||||
f.spams--
|
||||
fv = &f.spams
|
||||
}
|
||||
if *fv == 0 {
|
||||
f.log.Error("attempt to decrease ham/spam message count while already zero", slog.Bool("ham", ham))
|
||||
} else {
|
||||
*fv -= 1
|
||||
}
|
||||
|
||||
// Decrease the word counts.
|
||||
|
@ -633,10 +639,16 @@ func (f *Filter) Untrain(ctx context.Context, ham bool, words map[string]struct{
|
|||
if !ok {
|
||||
continue
|
||||
}
|
||||
var v *uint32
|
||||
if ham {
|
||||
c.Ham--
|
||||
v = &c.Ham
|
||||
} else {
|
||||
c.Spam--
|
||||
v = &c.Spam
|
||||
}
|
||||
if *v == 0 {
|
||||
f.log.Error("attempt to decrease ham/spam word count while already zero", slog.String("word", w), slog.Bool("ham", ham))
|
||||
} else {
|
||||
*v -= 1
|
||||
}
|
||||
f.cache[w] = c
|
||||
f.changed[w] = c
|
||||
|
|
|
@ -126,7 +126,7 @@ func TestFilter(t *testing.T) {
|
|||
tcheck(t, err, "train spam message")
|
||||
_, err = spamf.Seek(0, 0)
|
||||
tcheck(t, err, "seek spam message")
|
||||
err = f.TrainMessage(ctxbg, spamf, spamsize, true)
|
||||
err = f.TrainMessage(ctxbg, spamf, spamsize, false)
|
||||
tcheck(t, err, "train spam message")
|
||||
|
||||
if !f.modified {
|
||||
|
@ -166,16 +166,16 @@ func TestFilter(t *testing.T) {
|
|||
tcheck(t, err, "untrain ham message")
|
||||
_, err = hamf.Seek(0, 0)
|
||||
tcheck(t, err, "seek ham message")
|
||||
err = f.UntrainMessage(ctxbg, hamf, spamsize, true)
|
||||
err = f.UntrainMessage(ctxbg, hamf, hamsize, true)
|
||||
tcheck(t, err, "untrain ham message")
|
||||
|
||||
_, err = spamf.Seek(0, 0)
|
||||
tcheck(t, err, "seek spam message")
|
||||
err = f.UntrainMessage(ctxbg, spamf, spamsize, true)
|
||||
err = f.UntrainMessage(ctxbg, spamf, spamsize, false)
|
||||
tcheck(t, err, "untrain spam message")
|
||||
_, err = spamf.Seek(0, 0)
|
||||
tcheck(t, err, "seek spam message")
|
||||
err = f.UntrainMessage(ctxbg, spamf, spamsize, true)
|
||||
err = f.UntrainMessage(ctxbg, spamf, spamsize, false)
|
||||
tcheck(t, err, "untrain spam message")
|
||||
|
||||
if !f.modified {
|
||||
|
|
63
main.go
63
main.go
|
@ -1,7 +1,6 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
|
@ -738,7 +737,7 @@ func ctlcmdConfigDomainRemove(ctl *ctl, d dns.Domain) {
|
|||
|
||||
func cmdConfigAliasList(c *cmd) {
|
||||
c.params = "domain"
|
||||
c.help = `List aliases for domain.`
|
||||
c.help = `Show aliases (lists) for domain.`
|
||||
args := c.Parse()
|
||||
if len(args) != 1 {
|
||||
c.Usage()
|
||||
|
@ -757,7 +756,7 @@ func ctlcmdConfigAliasList(ctl *ctl, address string) {
|
|||
|
||||
func cmdConfigAliasPrint(c *cmd) {
|
||||
c.params = "alias"
|
||||
c.help = `Print settings and members of alias.`
|
||||
c.help = `Print settings and members of alias (list).`
|
||||
args := c.Parse()
|
||||
if len(args) != 1 {
|
||||
c.Usage()
|
||||
|
@ -776,7 +775,12 @@ func ctlcmdConfigAliasPrint(ctl *ctl, address string) {
|
|||
|
||||
func cmdConfigAliasAdd(c *cmd) {
|
||||
c.params = "alias@domain rcpt1@domain ..."
|
||||
c.help = `Add new alias with one or more addresses and public posting enabled.`
|
||||
c.help = `Add new alias (list) with one or more addresses and public posting enabled.
|
||||
|
||||
An alias is used for delivering incoming email to multiple recipients. If you
|
||||
want to add an address to an account, don't use an alias, just add the address
|
||||
to the account.
|
||||
`
|
||||
args := c.Parse()
|
||||
if len(args) < 2 {
|
||||
c.Usage()
|
||||
|
@ -797,7 +801,7 @@ func ctlcmdConfigAliasAdd(ctl *ctl, address string, alias config.Alias) {
|
|||
|
||||
func cmdConfigAliasUpdate(c *cmd) {
|
||||
c.params = "alias@domain [-postpublic false|true -listmembers false|true -allowmsgfrom false|true]"
|
||||
c.help = `Update alias configuration.`
|
||||
c.help = `Update alias (list) configuration.`
|
||||
var postpublic, listmembers, allowmsgfrom string
|
||||
c.flag.StringVar(&postpublic, "postpublic", "", "whether anyone or only list members can post")
|
||||
c.flag.StringVar(&listmembers, "listmembers", "", "whether list members can list members")
|
||||
|
@ -823,7 +827,7 @@ func ctlcmdConfigAliasUpdate(ctl *ctl, alias, postpublic, listmembers, allowmsgf
|
|||
|
||||
func cmdConfigAliasRemove(c *cmd) {
|
||||
c.params = "alias@domain"
|
||||
c.help = "Remove alias."
|
||||
c.help = "Remove alias (list)."
|
||||
args := c.Parse()
|
||||
if len(args) != 1 {
|
||||
c.Usage()
|
||||
|
@ -841,7 +845,7 @@ func ctlcmdConfigAliasRemove(ctl *ctl, alias string) {
|
|||
|
||||
func cmdConfigAliasAddaddr(c *cmd) {
|
||||
c.params = "alias@domain rcpt1@domain ..."
|
||||
c.help = `Add addresses to alias.`
|
||||
c.help = `Add addresses to alias (list).`
|
||||
args := c.Parse()
|
||||
if len(args) < 2 {
|
||||
c.Usage()
|
||||
|
@ -860,7 +864,7 @@ func ctlcmdConfigAliasAddaddr(ctl *ctl, alias string, addresses []string) {
|
|||
|
||||
func cmdConfigAliasRemoveaddr(c *cmd) {
|
||||
c.params = "alias@domain rcpt1@domain ..."
|
||||
c.help = `Remove addresses from alias.`
|
||||
c.help = `Remove addresses from alias (list).`
|
||||
args := c.Parse()
|
||||
if len(args) < 2 {
|
||||
c.Usage()
|
||||
|
@ -2875,19 +2879,23 @@ should be used, and how long the policy can be cached.
|
|||
}
|
||||
|
||||
func cmdRetrain(c *cmd) {
|
||||
c.params = "accountname"
|
||||
c.help = `Recreate and retrain the junk filter for the account.
|
||||
c.params = "[accountname]"
|
||||
c.help = `Recreate and retrain the junk filter for the account or all accounts.
|
||||
|
||||
Useful after having made changes to the junk filter configuration, or if the
|
||||
implementation has changed.
|
||||
`
|
||||
args := c.Parse()
|
||||
if len(args) != 1 {
|
||||
if len(args) > 1 {
|
||||
c.Usage()
|
||||
}
|
||||
var account string
|
||||
if len(args) == 1 {
|
||||
account = args[0]
|
||||
}
|
||||
|
||||
mustLoadConfig()
|
||||
ctlcmdRetrain(xctl(), args[0])
|
||||
ctlcmdRetrain(xctl(), account)
|
||||
}
|
||||
|
||||
func ctlcmdRetrain(ctl *ctl, account string) {
|
||||
|
@ -3519,35 +3527,10 @@ func cmdMessageParse(c *cmd) {
|
|||
err = enc.Encode(part)
|
||||
xcheckf(err, "write")
|
||||
|
||||
hasNonASCII := func(r io.Reader) bool {
|
||||
br := bufio.NewReader(r)
|
||||
for {
|
||||
b, err := br.ReadByte()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
xcheckf(err, "read header")
|
||||
if b > 0x7f {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var walk func(p *message.Part) bool
|
||||
walk = func(p *message.Part) bool {
|
||||
if hasNonASCII(p.HeaderReader()) {
|
||||
return true
|
||||
}
|
||||
for _, pp := range p.Parts {
|
||||
if walk(&pp) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if smtputf8 {
|
||||
fmt.Println("message needs smtputf8:", walk(&part))
|
||||
needs, err := part.NeedsSMTPUTF8()
|
||||
xcheckf(err, "checking if message needs smtputf8")
|
||||
fmt.Println("message needs smtputf8:", needs)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"net/textproto"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/text/encoding/ianaindex"
|
||||
|
||||
|
@ -598,6 +599,38 @@ func (p *Part) IsDSN() bool {
|
|||
(p.Parts[1].MediaSubType == "DELIVERY-STATUS" || p.Parts[1].MediaSubType == "GLOBAL-DELIVERY-STATUS")
|
||||
}
|
||||
|
||||
func hasNonASCII(r io.Reader) (bool, error) {
|
||||
br := bufio.NewReader(r)
|
||||
for {
|
||||
b, err := br.ReadByte()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if b > unicode.MaxASCII {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// NeedsSMTPUTF8 returns whether the part needs the SMTPUTF8 extension to be
|
||||
// transported, due to non-ascii in message headers.
|
||||
func (p *Part) NeedsSMTPUTF8() (bool, error) {
|
||||
if has, err := hasNonASCII(p.HeaderReader()); err != nil {
|
||||
return false, fmt.Errorf("reading header: %w", err)
|
||||
} else if has {
|
||||
return true, nil
|
||||
}
|
||||
for _, pp := range p.Parts {
|
||||
if has, err := pp.NeedsSMTPUTF8(); err != nil || has {
|
||||
return has, err
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var ErrParamEncoding = errors.New("bad header parameter encoding")
|
||||
|
||||
// DispositionFilename tries to parse the disposition header and the "filename"
|
||||
|
|
|
@ -783,7 +783,7 @@ func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, c
|
|||
}
|
||||
}
|
||||
if l.TLS.ACME != "" && (len(l.TLS.HostPrivateRSA2048Keys) == 0) != (len(l.TLS.HostPrivateECDSAP256Keys) == 0) {
|
||||
log.Error("warning: uncommon configuration with either only an RSA 2048 or ECDSA P256 host private key for DANE/ACME certificates; this ACME implementation can retrieve certificates for both type of keys, it is recommended to set either both or none; continuing")
|
||||
log.Warn("uncommon configuration with either only an RSA 2048 or ECDSA P256 host private key for DANE/ACME certificates; this ACME implementation can retrieve certificates for both type of keys, it is recommended to set either both or none; continuing")
|
||||
}
|
||||
|
||||
// TLS 1.2 was introduced in 2008. TLS <1.2 was deprecated by ../rfc/8996:31 and ../rfc/8997:66 in 2021.
|
||||
|
|
|
@ -3,6 +3,7 @@ package queue
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
|
@ -796,7 +797,7 @@ func Incoming(ctx context.Context, log mlog.Log, acc *store.Account, messageID s
|
|||
|
||||
log.Debug("composing webhook for incoming message")
|
||||
|
||||
structure, err := webhook.PartStructure(log, &part)
|
||||
structure, err := PartStructure(log, &part)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing part structure: %v", err)
|
||||
}
|
||||
|
@ -912,6 +913,38 @@ func Incoming(ctx context.Context, log mlog.Log, acc *store.Account, messageID s
|
|||
return nil
|
||||
}
|
||||
|
||||
// PartStructure returns a webhook.Structure for a parsed message part.
|
||||
func PartStructure(log mlog.Log, p *message.Part) (webhook.Structure, error) {
|
||||
parts := make([]webhook.Structure, len(p.Parts))
|
||||
for i := range p.Parts {
|
||||
var err error
|
||||
parts[i], err = PartStructure(log, &p.Parts[i])
|
||||
if err != nil && !errors.Is(err, message.ErrParamEncoding) {
|
||||
return webhook.Structure{}, err
|
||||
}
|
||||
}
|
||||
disp, filename, err := p.DispositionFilename()
|
||||
if err != nil && errors.Is(err, message.ErrParamEncoding) {
|
||||
log.Debugx("parsing disposition/filename", err)
|
||||
} else if err != nil {
|
||||
return webhook.Structure{}, err
|
||||
}
|
||||
s := webhook.Structure{
|
||||
ContentType: strings.ToLower(p.MediaType + "/" + p.MediaSubType),
|
||||
ContentTypeParams: p.ContentTypeParams,
|
||||
ContentID: p.ContentID,
|
||||
ContentDisposition: strings.ToLower(disp),
|
||||
Filename: filename,
|
||||
DecodedSize: p.DecodedSize,
|
||||
Parts: parts,
|
||||
}
|
||||
// Replace nil map with empty map, for easier to use JSON.
|
||||
if s.ContentTypeParams == nil {
|
||||
s.ContentTypeParams = map[string]string{}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func isAutomated(h textproto.MIMEHeader) bool {
|
||||
l := []string{"List-Id", "List-Unsubscribe", "List-Unsubscribe-Post", "Precedence"}
|
||||
for _, k := range l {
|
||||
|
|
|
@ -82,7 +82,7 @@ func TestHookIncoming(t *testing.T) {
|
|||
tcheck(t, err, "decode incoming webhook")
|
||||
in.Meta.Received = in.Meta.Received.Local() // For TZ UTC.
|
||||
|
||||
structure, err := webhook.PartStructure(pkglog, &part)
|
||||
structure, err := PartStructure(pkglog, &part)
|
||||
tcheck(t, err, "part structure")
|
||||
|
||||
expIncoming := webhook.Incoming{
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
|
@ -67,6 +68,8 @@ Quickstart writes configuration files, prints initial admin and account
|
|||
passwords, DNS records you should create. If you run it on Linux it writes a
|
||||
systemd service file and prints commands to enable and start mox as service.
|
||||
|
||||
All output is written to quickstart.log for later reference.
|
||||
|
||||
The user or uid is optional, defaults to "mox", and is the user or uid/gid mox
|
||||
will run as after initialization.
|
||||
|
||||
|
@ -105,6 +108,35 @@ output of "mox config describe-domains" and see the output of
|
|||
c.Usage()
|
||||
}
|
||||
|
||||
// Write all output to quickstart.log.
|
||||
logfile, err := os.Create("quickstart.log")
|
||||
xcheckf(err, "creating quickstart.log")
|
||||
|
||||
origStdout := os.Stdout
|
||||
origStderr := os.Stderr
|
||||
piper, pipew, err := os.Pipe()
|
||||
xcheckf(err, "creating pipe for logging to logfile")
|
||||
pipec := make(chan struct{})
|
||||
go func() {
|
||||
io.Copy(io.MultiWriter(origStdout, logfile), piper)
|
||||
close(pipec)
|
||||
}()
|
||||
// A single pipe, so writes to stdout and stderr don't get interleaved.
|
||||
os.Stdout = pipew
|
||||
os.Stderr = pipew
|
||||
logClose := func() {
|
||||
pipew.Close()
|
||||
<-pipec
|
||||
os.Stdout = origStdout
|
||||
os.Stderr = origStderr
|
||||
err := logfile.Close()
|
||||
xcheckf(err, "closing quickstart.log")
|
||||
}
|
||||
defer logClose()
|
||||
log.SetOutput(os.Stdout)
|
||||
fmt.Printf("(output is also written to quickstart.log)\n\n")
|
||||
defer fmt.Printf("\n(output is also written to quickstart.log)\n")
|
||||
|
||||
// We take care to cleanup created files when we error out.
|
||||
// We don't want to get a new user into trouble with half of the files
|
||||
// after encountering an error.
|
||||
|
@ -121,7 +153,9 @@ output of "mox config describe-domains" and see the output of
|
|||
}
|
||||
}
|
||||
|
||||
log.Fatalf(format, args...)
|
||||
log.Printf(format, args...)
|
||||
logClose()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
xwritefile := func(path string, data []byte, perm os.FileMode) {
|
||||
|
@ -710,6 +744,7 @@ many authentication failures).
|
|||
hostbase := filepath.FromSlash("path/to/" + dnshostname.Name())
|
||||
mtastsbase := filepath.FromSlash("path/to/mta-sts." + domain.Name())
|
||||
autoconfigbase := filepath.FromSlash("path/to/autoconfig." + domain.Name())
|
||||
mailbase := filepath.FromSlash("path/to/mail." + domain.Name())
|
||||
public.TLS = &config.TLS{
|
||||
KeyCerts: []config.KeyCert{
|
||||
{CertFile: hostbase + "-chain.crt.pem", KeyFile: hostbase + ".key.pem"},
|
||||
|
@ -717,6 +752,9 @@ many authentication failures).
|
|||
{CertFile: autoconfigbase + "-chain.crt.pem", KeyFile: autoconfigbase + ".key.pem"},
|
||||
},
|
||||
}
|
||||
if mailbase != hostbase {
|
||||
public.TLS.KeyCerts = append(public.TLS.KeyCerts, config.KeyCert{CertFile: mailbase + "-chain.crt.pem", KeyFile: mailbase + ".key.pem"})
|
||||
}
|
||||
|
||||
fmt.Println(
|
||||
`Placeholder paths to TLS certificates to be provided by the existing webserver
|
||||
|
|
|
@ -1956,47 +1956,32 @@ func (c *conn) cmdRcpt(p *parser) {
|
|||
c.bwritecodeline(smtp.C250Completed, smtp.SeAddr1Other0, "now on the list", nil)
|
||||
}
|
||||
|
||||
// ../rfc/6531:497
|
||||
func (c *conn) isSMTPUTF8Required(part *message.Part) bool {
|
||||
hasNonASCII := func(r io.Reader) bool {
|
||||
br := bufio.NewReader(r)
|
||||
for {
|
||||
b, err := br.ReadByte()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
xcheckf(err, "read header")
|
||||
if b > unicode.MaxASCII {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
var hasNonASCIIPartHeader func(p *message.Part) bool
|
||||
hasNonASCIIPartHeader = func(p *message.Part) bool {
|
||||
if hasNonASCII(p.HeaderReader()) {
|
||||
func hasNonASCII(s string) bool {
|
||||
for _, c := range []byte(s) {
|
||||
if c > unicode.MaxASCII {
|
||||
return true
|
||||
}
|
||||
for _, pp := range p.Parts {
|
||||
if hasNonASCIIPartHeader(&pp) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ../rfc/6531:497
|
||||
func (c *conn) isSMTPUTF8Required(part *message.Part) bool {
|
||||
// Check "MAIL FROM".
|
||||
if hasNonASCII(strings.NewReader(string(c.mailFrom.Localpart))) {
|
||||
if hasNonASCII(string(c.mailFrom.Localpart)) {
|
||||
return true
|
||||
}
|
||||
// Check all "RCPT TO".
|
||||
for _, rcpt := range c.recipients {
|
||||
if hasNonASCII(strings.NewReader(string(rcpt.Addr.Localpart))) {
|
||||
if hasNonASCII(string(rcpt.Addr.Localpart)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check header in all message parts.
|
||||
return hasNonASCIIPartHeader(part)
|
||||
smtputf8, err := part.NeedsSMTPUTF8()
|
||||
xcheckf(err, "checking if smtputf8 is required")
|
||||
return smtputf8
|
||||
}
|
||||
|
||||
// ../rfc/5321:1992 ../rfc/5321:1098
|
||||
|
@ -3502,7 +3487,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
|||
code = smtp.C554TransactionFailed
|
||||
}
|
||||
lines = append(lines, "multiple errors")
|
||||
xsmtpErrorf(code, secode, !serverError, strings.Join(lines, "\n"))
|
||||
xsmtpErrorf(code, secode, !serverError, "%s", strings.Join(lines, "\n"))
|
||||
}
|
||||
// Generate one DSN for all failed recipients.
|
||||
if len(deliverErrors) > 0 {
|
||||
|
|
|
@ -741,6 +741,12 @@ type Settings struct {
|
|||
|
||||
// Show HTML version of message by default, instead of plain text.
|
||||
ShowHTML bool
|
||||
|
||||
// If true, don't show shortcuts in webmail after mouse interaction.
|
||||
NoShowShortcuts bool
|
||||
|
||||
// Additional headers to display in message view. E.g. Delivered-To, User-Agent, X-Mox-Reason.
|
||||
ShowHeaders []string
|
||||
}
|
||||
|
||||
// ViewMode how a message should be viewed: its text parts, html parts, or html
|
||||
|
|
|
@ -117,7 +117,7 @@ func (a *Account) RetrainMessage(ctx context.Context, log mlog.Log, tx *bstore.T
|
|||
|
||||
words, err := jf.ParseMessage(p)
|
||||
if err != nil {
|
||||
log.Errorx("parsing message for updating junk filter", err, slog.Any("parse", ""))
|
||||
log.Infox("parsing message for updating junk filter", err, slog.Any("parse", ""))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -162,7 +162,7 @@ func (a *Account) TrainMessage(ctx context.Context, log mlog.Log, jf *junk.Filte
|
|||
|
||||
words, err := jf.ParseMessage(p)
|
||||
if err != nil {
|
||||
log.Errorx("parsing message for updating junk filter", err, slog.Any("parse", ""))
|
||||
log.Infox("parsing message for updating junk filter", err, slog.Any("parse", ""))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -2175,7 +2175,7 @@ const account = async (name) => {
|
|||
await check(fieldset, client.AddressAdd(address, name));
|
||||
form.reset();
|
||||
window.location.reload(); // todo: only reload the destinations
|
||||
}, fieldset = dom.fieldset(dom.label(style({ display: 'inline-block' }), dom.span('Localpart', attr.title('The localpart is the part before the "@"-sign of an email address. If empty, a catchall address is configured for the domain.')), dom.br(), localpart = dom.input()), '@', dom.label(style({ display: 'inline-block' }), dom.span('Domain'), dom.br(), domain = dom.select((domains || []).map(d => dom.option(domainName(d), domainName(d) === config.Domain ? attr.selected('') : [])))), ' ', dom.submitbutton('Add address'))), dom.br(), dom.h2('Aliases/lists'), dom.table(dom.thead(dom.tr(dom.th('Alias address', attr.title('Messages sent to this address will be delivered to all members of the alias/list. A member does not receive a message if their address is in the message From header.')), dom.th('Subscription address'), dom.th('Allowed senders', attr.title('Whether only members can send through the alias/list, or anyone.')), dom.th('Send as alias address', attr.title('If enabled, messages can be sent with the alias address in the message "From" header.')), dom.th('Members visible', attr.title('If enabled, members can see the addresses of other members.')))), (config.Aliases || []).length === 0 ? dom.tr(dom.td(attr.colspan('6'), 'None')) : [], (config.Aliases || []).sort((a, b) => a.Alias.LocalpartStr < b.Alias.LocalpartStr ? -1 : (domainName(a.Alias.Domain) < domainName(b.Alias.Domain) ? -1 : 1)).map(a => dom.tr(dom.td(dom.a(prewrap(a.Alias.LocalpartStr, '@', domainName(a.Alias.Domain)), attr.href('#domains/' + domainName(a.Alias.Domain) + '/alias/' + encodeURIComponent(a.Alias.LocalpartStr)))), dom.td(prewrap(a.SubscriptionAddress)), dom.td(a.Alias.PostPublic ? 'Anyone' : 'Members only'), dom.td(a.Alias.AllowMsgFrom ? 'Yes' : 'No'), dom.td(a.Alias.ListMembers ? 'Yes' : 'No'), dom.td(dom.clickbutton('Remove', async function click(e) {
|
||||
}, fieldset = dom.fieldset(dom.label(style({ display: 'inline-block' }), dom.span('Localpart', attr.title('The localpart is the part before the "@"-sign of an email address. If empty, a catchall address is configured for the domain.')), dom.br(), localpart = dom.input()), '@', dom.label(style({ display: 'inline-block' }), dom.span('Domain'), dom.br(), domain = dom.select((domains || []).map(d => dom.option(domainName(d), domainName(d) === config.Domain ? attr.selected('') : [])))), ' ', dom.submitbutton('Add address'))), dom.br(), dom.h2('Alias (list) membership'), dom.table(dom.thead(dom.tr(dom.th('Alias address', attr.title('Messages sent to this address will be delivered to all members of the alias/list. A member does not receive a message if their address is in the message From header.')), dom.th('Subscription address'), dom.th('Allowed senders', attr.title('Whether only members can send through the alias/list, or anyone.')), dom.th('Send as alias address', attr.title('If enabled, messages can be sent with the alias address in the message "From" header.')), dom.th('Members visible', attr.title('If enabled, members can see the addresses of other members.')))), (config.Aliases || []).length === 0 ? dom.tr(dom.td(attr.colspan('6'), 'None')) : [], (config.Aliases || []).sort((a, b) => a.Alias.LocalpartStr < b.Alias.LocalpartStr ? -1 : (domainName(a.Alias.Domain) < domainName(b.Alias.Domain) ? -1 : 1)).map(a => dom.tr(dom.td(dom.a(prewrap(a.Alias.LocalpartStr, '@', domainName(a.Alias.Domain)), attr.href('#domains/' + domainName(a.Alias.Domain) + '/alias/' + encodeURIComponent(a.Alias.LocalpartStr)))), dom.td(prewrap(a.SubscriptionAddress)), dom.td(a.Alias.PostPublic ? 'Anyone' : 'Members only'), dom.td(a.Alias.AllowMsgFrom ? 'Yes' : 'No'), dom.td(a.Alias.ListMembers ? 'Yes' : 'No'), dom.td(dom.clickbutton('Remove', async function click(e) {
|
||||
await check(e.target, client.AliasAddressesRemove(a.Alias.LocalpartStr, domainName(a.Alias.Domain), [a.SubscriptionAddress]));
|
||||
window.location.reload(); // todo: reload less
|
||||
}))))), dom.br(), dom.h2('Settings'), dom.form(fieldsetSettings = dom.fieldset(dom.label(style({ display: 'block', marginBottom: '.5ex' }), dom.span('Maximum outgoing messages per day', attr.title('Maximum number of outgoing messages for this account in a 24 hour window. This limits the damage to recipients and the reputation of this mail server in case of account compromise. Default 1000. MaxOutgoingMessagesPerDay in configuration file.')), dom.br(), maxOutgoingMessagesPerDay = dom.input(attr.type('number'), attr.required(''), attr.value('' + (config.MaxOutgoingMessagesPerDay || 1000)))), dom.label(style({ display: 'block', marginBottom: '.5ex' }), dom.span('Maximum first-time recipients per day', attr.title('Maximum number of first-time recipients in outgoing messages for this account in a 24 hour window. This limits the damage to recipients and the reputation of this mail server in case of account compromise. Default 200. MaxFirstTimeRecipientsPerDay in configuration file.')), dom.br(), maxFirstTimeRecipientsPerDay = dom.input(attr.type('number'), attr.required(''), attr.value('' + (config.MaxFirstTimeRecipientsPerDay || 200)))), dom.label(style({ display: 'block', marginBottom: '.5ex' }), dom.span('Disk usage quota: Maximum total message size ', attr.title('Default maximum total message size in bytes for the account, overriding any globally configured default maximum size if non-zero. A negative value can be used to have no limit in case there is a limit by default. Attempting to add new messages to an account beyond its maximum total size will result in an error. Useful to prevent a single account from filling storage. Use units "k" for kilobytes, or "m", "g", "t".')), dom.br(), quotaMessageSize = dom.input(attr.value(formatQuotaSize(config.QuotaMessageSize))), ' Current usage is ', formatQuotaSize(Math.floor(diskUsage / (1024 * 1024)) * 1024 * 1024), '.'), dom.div(style({ display: 'block', marginBottom: '.5ex' }), dom.label(firstTimeSenderDelay = dom.input(attr.type('checkbox'), config.NoFirstTimeSenderDelay ? [] : attr.checked('')), ' ', dom.span('Delay deliveries from first-time senders.', attr.title('To slow down potential spammers, when the message is misclassified as non-junk. Turning off the delay can be useful when the account processes messages automatically and needs fast responses.')))), dom.submitbutton('Save')), async function submit(e) {
|
||||
|
@ -2289,6 +2289,7 @@ const domain = async (d) => {
|
|||
let aliasFieldset;
|
||||
let aliasLocalpart;
|
||||
let aliasAddresses;
|
||||
let aliasAddText;
|
||||
let descrFieldset;
|
||||
let descrText;
|
||||
let clientSettingsDomainFieldset;
|
||||
|
@ -2364,9 +2365,9 @@ const domain = async (d) => {
|
|||
await check(addrFieldset, client.AddressAdd(addrLocalpart.value + '@' + d, addrAccount.value));
|
||||
addrForm.reset();
|
||||
window.location.reload(); // todo: only reload the addresses
|
||||
}, addrFieldset = dom.fieldset(dom.label(style({ display: 'inline-block' }), dom.span('Localpart', attr.title('The localpart is the part before the "@"-sign of an address. An empty localpart is the catchall destination/address for the domain.')), dom.br(), addrLocalpart = dom.input()), '@', domainName(dnsdomain), ' ', dom.label(style({ display: 'inline-block' }), dom.span('Account', attr.title('Account to assign the address to.')), dom.br(), addrAccount = dom.select(attr.required(''), (accounts || []).map(a => dom.option(a)))), ' ', dom.submitbutton('Add address', attr.title('Address will be added and the config reloaded.')))), dom.br(), dom.h2('Aliases/lists'), dom.table(dom.thead(dom.tr(dom.th('Address'), dom.th('Allowed senders', attr.title('Whether only members can send through the alias/list, or anyone.')), dom.th('Send as alias address', attr.title('If enabled, messages can be sent with the alias address in the message "From" header.')), dom.th('Members visible', attr.title('If enabled, members can see the addresses of other members.')))), Object.values(localpartAliases).length === 0 ? dom.tr(dom.td(attr.colspan('4'), 'None')) : [], Object.values(localpartAliases).sort((a, b) => a.LocalpartStr < b.LocalpartStr ? -1 : 1).map(a => {
|
||||
}, addrFieldset = dom.fieldset(dom.label(style({ display: 'inline-block' }), dom.span('Localpart', attr.title('The localpart is the part before the "@"-sign of an address. An empty localpart is the catchall destination/address for the domain.')), dom.br(), addrLocalpart = dom.input()), '@', domainName(dnsdomain), ' ', dom.label(style({ display: 'inline-block' }), dom.span('Account', attr.title('Account to assign the address to.')), dom.br(), addrAccount = dom.select(attr.required(''), (accounts || []).map(a => dom.option(a)))), ' ', dom.submitbutton('Add address', attr.title('Address will be added and the config reloaded.')))), dom.br(), dom.h2('Aliases (lists)'), dom.table(dom.thead(dom.tr(dom.th('Address'), dom.th('Allowed senders', attr.title('Whether only members can send through the alias/list, or anyone.')), dom.th('Send as alias address', attr.title('If enabled, messages can be sent with the alias address in the message "From" header.')), dom.th('Members visible', attr.title('If enabled, members can see the addresses of other members.')))), Object.values(localpartAliases).length === 0 ? dom.tr(dom.td(attr.colspan('4'), 'None')) : [], Object.values(localpartAliases).sort((a, b) => a.LocalpartStr < b.LocalpartStr ? -1 : 1).map(a => {
|
||||
return dom.tr(dom.td(dom.a(prewrap(a.LocalpartStr), attr.href('#domains/' + d + '/alias/' + encodeURIComponent(a.LocalpartStr)))), dom.td(a.PostPublic ? 'Anyone' : 'Members only'), dom.td(a.AllowMsgFrom ? 'Yes' : 'No'), dom.td(a.ListMembers ? 'Yes' : 'No'));
|
||||
})), dom.br(), dom.h2('Add alias'), dom.form(async function submit(e) {
|
||||
})), dom.br(), dom.h2('Add alias (list)'), dom.form(async function submit(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const alias = {
|
||||
|
@ -2380,7 +2381,10 @@ const domain = async (d) => {
|
|||
};
|
||||
await check(aliasFieldset, client.AliasAdd(aliasLocalpart.value, d, alias));
|
||||
window.location.hash = '#domains/' + d + '/alias/' + encodeURIComponent(aliasLocalpart.value);
|
||||
}, aliasFieldset = dom.fieldset(style({ display: 'flex', alignItems: 'flex-start', gap: '1em' }), dom.label(dom.div('Localpart', attr.title('The localpart is the part before the "@"-sign of an address.')), aliasLocalpart = dom.input(attr.required('')), '@', domainName(dnsdomain), ' '), dom.label(dom.div('Addresses', attr.title('One members address per line, full address of form localpart@domain. At least one address required.')), aliasAddresses = dom.textarea(attr.required(''), attr.rows('1'), function focus() { aliasAddresses.setAttribute('rows', '5'); })), dom.div(dom.div('\u00a0'), dom.submitbutton('Add alias', attr.title('Alias will be added and the config reloaded.'))))), dom.br(), RoutesEditor('domain-specific', transports, domainConfig.Routes || [], async (routes) => await client.DomainRoutesSave(d, routes)), dom.br(), dom.h2('Settings'), dom.form(async function submit(e) {
|
||||
}, aliasFieldset = dom.fieldset(style({ display: 'flex', alignItems: 'flex-start', gap: '1em' }), dom.label(dom.div('Localpart', attr.title('The localpart is the part before the "@"-sign of an address.')), aliasLocalpart = dom.input(attr.required('')), '@', domainName(dnsdomain), ' '), dom.label(dom.div('Addresses', attr.title('One members address per line, full address of form localpart@domain. At least one address required.')), aliasAddresses = dom.textarea(attr.required(''), attr.rows('1'), function focus() {
|
||||
aliasAddresses.setAttribute('rows', '5');
|
||||
aliasAddText.style.visibility = 'visible';
|
||||
})), dom.div(dom.div('\u00a0'), dom.submitbutton('Add alias', attr.title('Alias will be added and the config reloaded.')), aliasAddText = dom.p(style({ visibility: 'hidden', fontStyle: 'italic' }), 'Messages sent to aliases are delivered to each member address of the alias, like a mailing list. For an additional address for an account, add it as regular address (see above).')))), dom.br(), RoutesEditor('domain-specific', transports, domainConfig.Routes || [], async (routes) => await client.DomainRoutesSave(d, routes)), dom.br(), dom.h2('Settings'), dom.form(async function submit(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
await check(descrFieldset, client.DomainDescriptionSave(d, descrText.value));
|
||||
|
|
|
@ -884,7 +884,7 @@ const account = async (name: string) => {
|
|||
),
|
||||
dom.br(),
|
||||
|
||||
dom.h2('Aliases/lists'),
|
||||
dom.h2('Alias (list) membership'),
|
||||
dom.table(
|
||||
dom.thead(
|
||||
dom.tr(
|
||||
|
@ -1094,6 +1094,7 @@ const domain = async (d: string) => {
|
|||
let aliasFieldset: HTMLFieldSetElement
|
||||
let aliasLocalpart: HTMLInputElement
|
||||
let aliasAddresses: HTMLTextAreaElement
|
||||
let aliasAddText: HTMLElement
|
||||
|
||||
let descrFieldset: HTMLFieldSetElement
|
||||
let descrText: HTMLInputElement
|
||||
|
@ -1347,7 +1348,7 @@ const domain = async (d: string) => {
|
|||
),
|
||||
dom.br(),
|
||||
|
||||
dom.h2('Aliases/lists'),
|
||||
dom.h2('Aliases (lists)'),
|
||||
dom.table(
|
||||
dom.thead(
|
||||
dom.tr(
|
||||
|
@ -1368,7 +1369,7 @@ const domain = async (d: string) => {
|
|||
}),
|
||||
),
|
||||
dom.br(),
|
||||
dom.h2('Add alias'),
|
||||
dom.h2('Add alias (list)'),
|
||||
dom.form(
|
||||
async function submit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
|
@ -1395,11 +1396,15 @@ const domain = async (d: string) => {
|
|||
),
|
||||
dom.label(
|
||||
dom.div('Addresses', attr.title('One members address per line, full address of form localpart@domain. At least one address required.')),
|
||||
aliasAddresses=dom.textarea(attr.required(''), attr.rows('1'), function focus() { aliasAddresses.setAttribute('rows', '5') }),
|
||||
aliasAddresses=dom.textarea(attr.required(''), attr.rows('1'), function focus() {
|
||||
aliasAddresses.setAttribute('rows', '5')
|
||||
aliasAddText.style.visibility = 'visible'
|
||||
}),
|
||||
),
|
||||
dom.div(
|
||||
dom.div('\u00a0'),
|
||||
dom.submitbutton('Add alias', attr.title('Alias will be added and the config reloaded.')),
|
||||
aliasAddText=dom.p(style({visibility: 'hidden', fontStyle: 'italic'}), 'Messages sent to aliases are delivered to each member address of the alias, like a mailing list. For an additional address for an account, add it as regular address (see above).'),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -44,7 +44,6 @@ import (
|
|||
"github.com/mjl-/mox/store"
|
||||
"github.com/mjl-/mox/webapi"
|
||||
"github.com/mjl-/mox/webauth"
|
||||
"github.com/mjl-/mox/webhook"
|
||||
"github.com/mjl-/mox/webops"
|
||||
)
|
||||
|
||||
|
@ -1263,7 +1262,7 @@ func (s server) MessageGet(ctx context.Context, req webapi.MessageGetRequest) (r
|
|||
MailboxName: mb.Name,
|
||||
}
|
||||
|
||||
structure, err := webhook.PartStructure(log, &p)
|
||||
structure, err := queue.PartStructure(log, &p)
|
||||
xcheckf(err, "parsing structure")
|
||||
|
||||
result := webapi.MessageGetResult{
|
||||
|
|
|
@ -25,7 +25,6 @@ import (
|
|||
"github.com/mjl-/mox/queue"
|
||||
"github.com/mjl-/mox/store"
|
||||
"github.com/mjl-/mox/webapi"
|
||||
"github.com/mjl-/mox/webhook"
|
||||
)
|
||||
|
||||
var ctxbg = context.Background()
|
||||
|
@ -418,7 +417,7 @@ func TestServer(t *testing.T) {
|
|||
tcheckf(t, err, "reading raw message")
|
||||
part, err := message.EnsurePart(log.Logger, true, bytes.NewReader(b.Bytes()), int64(b.Len()))
|
||||
tcheckf(t, err, "parsing raw message")
|
||||
structure, err := webhook.PartStructure(log, &part)
|
||||
structure, err := queue.PartStructure(log, &part)
|
||||
tcheckf(t, err, "part structure")
|
||||
tcompare(t, structure, msgRes.Structure)
|
||||
|
||||
|
|
|
@ -8,12 +8,7 @@
|
|||
package webhook
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mjl-/mox/message"
|
||||
"github.com/mjl-/mox/mlog"
|
||||
)
|
||||
|
||||
// OutgoingEvent is an activity for an outgoing delivery. Either generated by the
|
||||
|
@ -145,35 +140,3 @@ type Structure struct {
|
|||
DecodedSize int64 // Size of content after decoding content-transfer-encoding. For text and HTML parts, this can be larger than the data returned since this size includes \r\n line endings.
|
||||
Parts []Structure // Subparts of a multipart message, possibly recursive.
|
||||
}
|
||||
|
||||
// PartStructure returns a Structure for a parsed message part.
|
||||
func PartStructure(log mlog.Log, p *message.Part) (Structure, error) {
|
||||
parts := make([]Structure, len(p.Parts))
|
||||
for i := range p.Parts {
|
||||
var err error
|
||||
parts[i], err = PartStructure(log, &p.Parts[i])
|
||||
if err != nil && !errors.Is(err, message.ErrParamEncoding) {
|
||||
return Structure{}, err
|
||||
}
|
||||
}
|
||||
disp, filename, err := p.DispositionFilename()
|
||||
if err != nil && errors.Is(err, message.ErrParamEncoding) {
|
||||
log.Debugx("parsing disposition/filename", err)
|
||||
} else if err != nil {
|
||||
return Structure{}, err
|
||||
}
|
||||
s := Structure{
|
||||
ContentType: strings.ToLower(p.MediaType + "/" + p.MediaSubType),
|
||||
ContentTypeParams: p.ContentTypeParams,
|
||||
ContentID: p.ContentID,
|
||||
ContentDisposition: strings.ToLower(disp),
|
||||
Filename: filename,
|
||||
DecodedSize: p.DecodedSize,
|
||||
Parts: parts,
|
||||
}
|
||||
// Replace nil map with empty map, for easier to use JSON.
|
||||
if s.ContentTypeParams == nil {
|
||||
s.ContentTypeParams = map[string]string{}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
|
|
@ -1757,6 +1757,21 @@
|
|||
"Typewords": [
|
||||
"bool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "NoShowShortcuts",
|
||||
"Docs": "If true, don't show shortcuts in webmail after mouse interaction.",
|
||||
"Typewords": [
|
||||
"bool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "ShowHeaders",
|
||||
"Docs": "Additional headers to display in message view. E.g. Delivered-To, User-Agent, X-Mox-Reason.",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"string"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -220,6 +220,8 @@ export interface Settings {
|
|||
Quoting: Quoting
|
||||
ShowAddressSecurity: boolean // Whether to show the bars underneath the address input fields indicating starttls/dnssec/dane/mtasts/requiretls support by address.
|
||||
ShowHTML: boolean // Show HTML version of message by default, instead of plain text.
|
||||
NoShowShortcuts: boolean // If true, don't show shortcuts in webmail after mouse interaction.
|
||||
ShowHeaders?: string[] | null // Additional headers to display in message view. E.g. Delivered-To, User-Agent, X-Mox-Reason.
|
||||
}
|
||||
|
||||
export interface Ruleset {
|
||||
|
@ -604,7 +606,7 @@ export const types: TypenameMap = {
|
|||
"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":"STARTTLS","Docs":"","Typewords":["SecurityResult"]},{"Name":"MTASTS","Docs":"","Typewords":["SecurityResult"]},{"Name":"DNSSEC","Docs":"","Typewords":["SecurityResult"]},{"Name":"DANE","Docs":"","Typewords":["SecurityResult"]},{"Name":"RequireTLS","Docs":"","Typewords":["SecurityResult"]}]},
|
||||
"Settings": {"Name":"Settings","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["uint8"]},{"Name":"Signature","Docs":"","Typewords":["string"]},{"Name":"Quoting","Docs":"","Typewords":["Quoting"]},{"Name":"ShowAddressSecurity","Docs":"","Typewords":["bool"]},{"Name":"ShowHTML","Docs":"","Typewords":["bool"]}]},
|
||||
"Settings": {"Name":"Settings","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["uint8"]},{"Name":"Signature","Docs":"","Typewords":["string"]},{"Name":"Quoting","Docs":"","Typewords":["Quoting"]},{"Name":"ShowAddressSecurity","Docs":"","Typewords":["bool"]},{"Name":"ShowHTML","Docs":"","Typewords":["bool"]},{"Name":"NoShowShortcuts","Docs":"","Typewords":["bool"]},{"Name":"ShowHeaders","Docs":"","Typewords":["[]","string"]}]},
|
||||
"Ruleset": {"Name":"Ruleset","Docs":"","Fields":[{"Name":"SMTPMailFromRegexp","Docs":"","Typewords":["string"]},{"Name":"MsgFromRegexp","Docs":"","Typewords":["string"]},{"Name":"VerifiedDomain","Docs":"","Typewords":["string"]},{"Name":"HeadersRegexp","Docs":"","Typewords":["{}","string"]},{"Name":"IsForward","Docs":"","Typewords":["bool"]},{"Name":"ListAllowDomain","Docs":"","Typewords":["string"]},{"Name":"AcceptRejectsToMailbox","Docs":"","Typewords":["string"]},{"Name":"Mailbox","Docs":"","Typewords":["string"]},{"Name":"Comment","Docs":"","Typewords":["string"]},{"Name":"VerifiedDNSDomain","Docs":"","Typewords":["Domain"]},{"Name":"ListAllowDNSDomain","Docs":"","Typewords":["Domain"]}]},
|
||||
"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"]},{"Name":"RejectsMailbox","Docs":"","Typewords":["string"]},{"Name":"Settings","Docs":"","Typewords":["Settings"]},{"Name":"AccountPath","Docs":"","Typewords":["string"]},{"Name":"Version","Docs":"","Typewords":["string"]}]},
|
||||
"DomainAddressConfig": {"Name":"DomainAddressConfig","Docs":"","Fields":[{"Name":"LocalpartCatchallSeparator","Docs":"","Typewords":["string"]},{"Name":"LocalpartCaseSensitive","Docs":"","Typewords":["bool"]}]},
|
||||
|
|
|
@ -311,7 +311,7 @@ var api;
|
|||
"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": "STARTTLS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["SecurityResult"] }] },
|
||||
"Settings": { "Name": "Settings", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["uint8"] }, { "Name": "Signature", "Docs": "", "Typewords": ["string"] }, { "Name": "Quoting", "Docs": "", "Typewords": ["Quoting"] }, { "Name": "ShowAddressSecurity", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHTML", "Docs": "", "Typewords": ["bool"] }] },
|
||||
"Settings": { "Name": "Settings", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["uint8"] }, { "Name": "Signature", "Docs": "", "Typewords": ["string"] }, { "Name": "Quoting", "Docs": "", "Typewords": ["Quoting"] }, { "Name": "ShowAddressSecurity", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHTML", "Docs": "", "Typewords": ["bool"] }, { "Name": "NoShowShortcuts", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHeaders", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||
"Ruleset": { "Name": "Ruleset", "Docs": "", "Fields": [{ "Name": "SMTPMailFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "HeadersRegexp", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListAllowDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "AcceptRejectsToMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Comment", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "ListAllowDNSDomain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||
"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"] }, { "Name": "RejectsMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Settings", "Docs": "", "Typewords": ["Settings"] }, { "Name": "AccountPath", "Docs": "", "Typewords": ["string"] }, { "Name": "Version", "Docs": "", "Typewords": ["string"] }] },
|
||||
"DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] },
|
||||
|
|
|
@ -311,7 +311,7 @@ var api;
|
|||
"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": "STARTTLS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["SecurityResult"] }] },
|
||||
"Settings": { "Name": "Settings", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["uint8"] }, { "Name": "Signature", "Docs": "", "Typewords": ["string"] }, { "Name": "Quoting", "Docs": "", "Typewords": ["Quoting"] }, { "Name": "ShowAddressSecurity", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHTML", "Docs": "", "Typewords": ["bool"] }] },
|
||||
"Settings": { "Name": "Settings", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["uint8"] }, { "Name": "Signature", "Docs": "", "Typewords": ["string"] }, { "Name": "Quoting", "Docs": "", "Typewords": ["Quoting"] }, { "Name": "ShowAddressSecurity", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHTML", "Docs": "", "Typewords": ["bool"] }, { "Name": "NoShowShortcuts", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHeaders", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||
"Ruleset": { "Name": "Ruleset", "Docs": "", "Fields": [{ "Name": "SMTPMailFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "HeadersRegexp", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListAllowDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "AcceptRejectsToMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Comment", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "ListAllowDNSDomain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||
"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"] }, { "Name": "RejectsMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Settings", "Docs": "", "Typewords": ["Settings"] }, { "Name": "AccountPath", "Docs": "", "Typewords": ["string"] }, { "Name": "Version", "Docs": "", "Typewords": ["string"] }] },
|
||||
"DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] },
|
||||
|
|
|
@ -311,7 +311,7 @@ var api;
|
|||
"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": "STARTTLS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["SecurityResult"] }] },
|
||||
"Settings": { "Name": "Settings", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["uint8"] }, { "Name": "Signature", "Docs": "", "Typewords": ["string"] }, { "Name": "Quoting", "Docs": "", "Typewords": ["Quoting"] }, { "Name": "ShowAddressSecurity", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHTML", "Docs": "", "Typewords": ["bool"] }] },
|
||||
"Settings": { "Name": "Settings", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["uint8"] }, { "Name": "Signature", "Docs": "", "Typewords": ["string"] }, { "Name": "Quoting", "Docs": "", "Typewords": ["Quoting"] }, { "Name": "ShowAddressSecurity", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHTML", "Docs": "", "Typewords": ["bool"] }, { "Name": "NoShowShortcuts", "Docs": "", "Typewords": ["bool"] }, { "Name": "ShowHeaders", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||
"Ruleset": { "Name": "Ruleset", "Docs": "", "Fields": [{ "Name": "SMTPMailFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "HeadersRegexp", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListAllowDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "AcceptRejectsToMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Comment", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "ListAllowDNSDomain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||
"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"] }, { "Name": "RejectsMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Settings", "Docs": "", "Typewords": ["Settings"] }, { "Name": "AccountPath", "Docs": "", "Typewords": ["string"] }, { "Name": "Version", "Docs": "", "Typewords": ["string"] }] },
|
||||
"DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] },
|
||||
|
@ -1493,10 +1493,6 @@ To simulate slow API calls and SSE events:
|
|||
|
||||
localStorage.setItem('sherpats-debug', JSON.stringify({waitMinMsec: 2000, waitMaxMsec: 4000}))
|
||||
|
||||
Show additional headers of messages:
|
||||
|
||||
settingsPut({...settings, showHeaders: ['Delivered-To', 'User-Agent', 'X-Mailer', 'Message-Id', 'List-Id', 'List-Post', 'X-Mox-Reason', 'TLS-Required']})
|
||||
|
||||
Enable logging and reload afterwards:
|
||||
|
||||
localStorage.setItem('log', 'yes')
|
||||
|
@ -1571,7 +1567,6 @@ try {
|
|||
catch (err) { }
|
||||
let accountSettings;
|
||||
const defaultSettings = {
|
||||
showShortcuts: true,
|
||||
mailboxesWidth: 240,
|
||||
layout: 'auto',
|
||||
leftWidthPct: 50,
|
||||
|
@ -1584,7 +1579,6 @@ const defaultSettings = {
|
|||
ignoreErrorsUntil: 0,
|
||||
mailboxCollapsed: {},
|
||||
showAllHeaders: false,
|
||||
showHeaders: [],
|
||||
threading: api.ThreadMode.ThreadOn,
|
||||
checkConsistency: location.hostname === 'localhost',
|
||||
composeWidth: 0,
|
||||
|
@ -1619,13 +1613,6 @@ const parseSettings = () => {
|
|||
if (!mailboxCollapsed || typeof mailboxCollapsed !== 'object') {
|
||||
mailboxCollapsed = def.mailboxCollapsed;
|
||||
}
|
||||
const getStringArray = (k) => {
|
||||
const v = x[k];
|
||||
if (v && Array.isArray(v) && (v.length === 0 || typeof v[0] === 'string')) {
|
||||
return v;
|
||||
}
|
||||
return def[k];
|
||||
};
|
||||
return {
|
||||
refine: getString('refine'),
|
||||
orderAsc: getBool('orderAsc'),
|
||||
|
@ -1637,10 +1624,8 @@ const parseSettings = () => {
|
|||
msglistfromPct: getInt('msglistfromPct'),
|
||||
ignoreErrorsUntil: getInt('ignoreErrorsUntil'),
|
||||
layout: getString('layout', 'auto', 'leftright', 'topbottom'),
|
||||
showShortcuts: getBool('showShortcuts'),
|
||||
mailboxCollapsed: mailboxCollapsed,
|
||||
showAllHeaders: getBool('showAllHeaders'),
|
||||
showHeaders: getStringArray('showHeaders'),
|
||||
threading: getString('threading', api.ThreadMode.ThreadOff, api.ThreadMode.ThreadOn, api.ThreadMode.ThreadUnread),
|
||||
checkConsistency: getBool('checkConsistency'),
|
||||
composeWidth: getInt('composeWidth'),
|
||||
|
@ -1764,7 +1749,7 @@ const envelopeIdentity = (l) => {
|
|||
let shortcutElem = dom.div(css('shortcutFlash', { fontSize: '2em', position: 'absolute', left: '.25em', bottom: '.25em', backgroundColor: '#888', padding: '0.25em .5em', color: 'white', borderRadius: '.15em' }));
|
||||
let shortcutTimer = 0;
|
||||
const showShortcut = (c) => {
|
||||
if (!settings.showShortcuts) {
|
||||
if (accountSettings?.NoShowShortcuts) {
|
||||
return;
|
||||
}
|
||||
if (shortcutTimer) {
|
||||
|
@ -2446,6 +2431,8 @@ const cmdSettings = async () => {
|
|||
let quoting;
|
||||
let showAddressSecurity;
|
||||
let showHTML;
|
||||
let showShortcuts;
|
||||
let showHeaders;
|
||||
if (!accountSettings) {
|
||||
throw new Error('No account settings fetched yet.');
|
||||
}
|
||||
|
@ -2458,15 +2445,43 @@ const cmdSettings = async () => {
|
|||
Quoting: quoting.value,
|
||||
ShowAddressSecurity: showAddressSecurity.checked,
|
||||
ShowHTML: showHTML.checked,
|
||||
NoShowShortcuts: !showShortcuts.checked,
|
||||
ShowHeaders: showHeaders.value.split('\n').map(s => s.trim()).filter(s => !!s),
|
||||
};
|
||||
await withDisabled(fieldset, client.SettingsSave(accSet));
|
||||
accountSettings = accSet;
|
||||
remove();
|
||||
}, fieldset = dom.fieldset(dom.label(style({ margin: '1ex 0', display: 'block' }), dom.div('Signature'), signature = dom.textarea(new String(accountSettings.Signature), style({ width: '100%' }), attr.rows('' + Math.max(3, 1 + accountSettings.Signature.split('\n').length)))), dom.label(style({ margin: '1ex 0', display: 'block' }), dom.div('Reply above/below original'), attr.title('Auto: If text is selected, only the replied text is quoted and editing starts below. Otherwise, the full message is quoted and editing starts at the top.'), quoting = dom.select(dom.option(attr.value(''), 'Auto'), dom.option(attr.value('bottom'), 'Bottom', accountSettings.Quoting === api.Quoting.Bottom ? attr.selected('') : []), dom.option(attr.value('top'), 'Top', accountSettings.Quoting === api.Quoting.Top ? attr.selected('') : []))), dom.label(style({ margin: '1ex 0', display: 'block' }), showAddressSecurity = dom.input(attr.type('checkbox'), accountSettings.ShowAddressSecurity ? attr.checked('') : []), ' Show address security indications', attr.title('Show bars underneath address input fields, indicating support for STARTTLS/DNSSEC/DANE/MTA-STS/RequireTLS.')), dom.label(style({ margin: '1ex 0', display: 'block' }), showHTML = dom.input(attr.type('checkbox'), accountSettings.ShowHTML ? attr.checked('') : []), ' Show HTML instead of text version by default'), dom.br(), dom.div(dom.submitbutton('Save')))));
|
||||
}, fieldset = dom.fieldset(dom.label(style({ margin: '1ex 0', display: 'block' }), dom.div('Signature'), signature = dom.textarea(new String(accountSettings.Signature), style({ width: '100%' }), attr.rows('' + Math.max(3, 1 + accountSettings.Signature.split('\n').length)))), dom.label(style({ margin: '1ex 0', display: 'block' }), dom.div('Reply above/below original'), attr.title('Auto: If text is selected, only the replied text is quoted and editing starts below. Otherwise, the full message is quoted and editing starts at the top.'), quoting = dom.select(dom.option(attr.value(''), 'Auto'), dom.option(attr.value('bottom'), 'Bottom', accountSettings.Quoting === api.Quoting.Bottom ? attr.selected('') : []), dom.option(attr.value('top'), 'Top', accountSettings.Quoting === api.Quoting.Top ? attr.selected('') : []))), dom.label(style({ margin: '1ex 0', display: 'block' }), showAddressSecurity = dom.input(attr.type('checkbox'), accountSettings.ShowAddressSecurity ? attr.checked('') : []), ' Show address security indications', attr.title('Show bars underneath address input fields, indicating support for STARTTLS/DNSSEC/DANE/MTA-STS/RequireTLS.')), dom.label(style({ margin: '1ex 0', display: 'block' }), showHTML = dom.input(attr.type('checkbox'), accountSettings.ShowHTML ? attr.checked('') : []), ' Show email as HTML instead of text by default for first-time senders', attr.title('Whether to show HTML or text is remembered per sender. This sets the default for unknown correspondents.')), dom.label(style({ margin: '1ex 0', display: 'block' }), showShortcuts = dom.input(attr.type('checkbox'), accountSettings.NoShowShortcuts ? [] : attr.checked('')), ' Show shortcut keys in bottom left after interaction with mouse'), dom.label(style({ margin: '1ex 0', display: 'block' }), dom.div('Show additional headers'), showHeaders = dom.textarea(new String((accountSettings.ShowHeaders || []).join('\n')), style({ width: '100%' }), attr.rows('' + Math.max(3, 1 + (accountSettings.ShowHeaders || []).length))), dom.div(style({ fontStyle: 'italic' }), 'One header name per line, for example Delivered-To, X-Mox-Reason, User-Agent, ...')), dom.div(style({ marginTop: '2ex' }), 'Register "mailto:" links with the browser/operating system to compose a message in webmail.', dom.br(), dom.clickbutton('Register', attr.title('In most browsers, registering is only allowed on HTTPS URLs. Your browser may ask for confirmation. If nothing appears to happen, the registration may already have been present.'), function click() {
|
||||
if (!window.navigator.registerProtocolHandler) {
|
||||
window.alert('Registering a protocol handler ("mailto:") is not supported by your browser.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.navigator.registerProtocolHandler('mailto', '#compose %s');
|
||||
window.alert('"mailto:"-links have been registered');
|
||||
}
|
||||
catch (err) {
|
||||
window.alert('Error registering "mailto:" protocol handler: ' + errmsg(err));
|
||||
}
|
||||
}), ' ', dom.clickbutton('Unregister', attr.title('Not all browsers implement unregistering via JavaScript.'), function click() {
|
||||
// Not supported on firefox at the time of writing, and the signature is not in the types.
|
||||
if (!window.navigator.unregisterProtocolHandler) {
|
||||
window.alert('Unregistering a protocol handler ("mailto:") via JavaScript is not supported by your browser. See your browser settings to unregister.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.navigator.unregisterProtocolHandler('mailto', '#compose %s');
|
||||
}
|
||||
catch (err) {
|
||||
window.alert('Error unregistering "mailto:" protocol handler: ' + errmsg(err));
|
||||
return;
|
||||
}
|
||||
window.alert('"mailto:" protocol handler unregistered.');
|
||||
})), dom.br(), dom.div(dom.submitbutton('Save')))));
|
||||
};
|
||||
// Show help popup, with shortcuts and basic explanation.
|
||||
const cmdHelp = async () => {
|
||||
const remove = popup(css('popupHelp', { padding: '1em 1em 2em 1em' }), dom.h1('Help and keyboard shortcuts'), dom.div(style({ display: 'flex' }), dom.div(style({ width: '40em' }), dom.table(dom.tr(dom.td(attr.colspan('2'), dom.h2('Global', style({ margin: '0' })))), [
|
||||
popup(css('popupHelp', { padding: '1em 1em 2em 1em' }), dom.h1('Help and keyboard shortcuts'), dom.div(style({ display: 'flex' }), dom.div(style({ width: '40em' }), dom.table(dom.tr(dom.td(attr.colspan('2'), dom.h2('Global', style({ margin: '0' })))), [
|
||||
['c', 'compose new message'],
|
||||
['/', 'search'],
|
||||
['i', 'open inbox'],
|
||||
|
@ -2477,7 +2492,7 @@ const cmdHelp = async () => {
|
|||
['←', 'collapse'],
|
||||
['→', 'expand'],
|
||||
['b', 'show more actions'],
|
||||
].map(t => dom.tr(dom.td(t[0]), dom.td(t[1]))), dom.tr(dom.td(attr.colspan('2'), dom.h2('Message list', style({ margin: '1ex 0 0 0' })))), dom.tr(dom.td('↓', ', j'), dom.td('down one message'), dom.td(attr.rowspan('6'), css('helpSideNote', { color: '#888', borderLeft: '2px solid', borderLeftColor: '#888', paddingLeft: '.5em' }), 'hold ctrl to only move focus', dom.br(), 'hold shift to expand selection')), [
|
||||
].map(t => dom.tr(dom.td(t[0]), dom.td(t[1]))), dom.tr(dom.td(attr.colspan('2'), dom.h2('Message list', style({ margin: '1ex 0 0 0' })))), dom.tr(dom.td('↓', ', j'), dom.td('down one message'), dom.td(attr.rowspan('6'), css('helpSideNote', { color: '#888', borderLeft: '2px solid', borderLeftColor: '#888', paddingLeft: '.5em' }), dom.div('hold ctrl to only move focus', attr.title('ctrl-l and ctrl-u are left for the browser the handle')), dom.div('hold shift to expand selection'))), [
|
||||
[['↑', ', k'], 'up one message'],
|
||||
['PageDown, l', 'down one screen'],
|
||||
['PageUp, h', 'up one screen'],
|
||||
|
@ -2523,7 +2538,7 @@ const cmdHelp = async () => {
|
|||
['O', 'show raw message'],
|
||||
['ctrl p', 'print message'],
|
||||
['I', 'toggle internals'],
|
||||
['ctrl I', 'toggle all headers'],
|
||||
['ctrl i', 'toggle all headers'],
|
||||
['alt k, alt ArrowUp', 'scroll up'],
|
||||
['alt j, alt ArrowDown', 'scroll down'],
|
||||
['alt K', 'scroll to top'],
|
||||
|
@ -2534,42 +2549,7 @@ const cmdHelp = async () => {
|
|||
['0', 'first attachment'],
|
||||
['$', 'next attachment'],
|
||||
['d', 'download'],
|
||||
].map(t => dom.tr(dom.td(t[0]), dom.td(t[1])))), dom.div(style({ marginTop: '2ex', marginBottom: '1ex' }), dom.span('Underdotted text', attr.title('Underdotted text shows additional information on hover.')), ' show an explanation or additional information when hovered.'), dom.div(style({ marginBottom: '1ex' }), 'Multiple messages can be selected by clicking messages while holding the control and/or shift keys. Dragging messages and dropping them on a mailbox moves the messages to that mailbox.'), dom.div(style({ marginBottom: '1ex' }), 'Text that changes ', dom.span(attr.title('Unicode blocks, e.g. from basic latin to cyrillic, or to emoticons.'), '"character groups"'), ' without whitespace has an ', dom.span(dom._class('scriptswitch'), 'orange underline'), ', which can be a sign of an intent to mislead (e.g. phishing).'), settings.showShortcuts ?
|
||||
dom.div(style({ marginTop: '2ex' }), 'Shortcut keys for mouse operation are shown in the bottom left. ', dom.clickbutton('Disable', function click() {
|
||||
settingsPut({ ...settings, showShortcuts: false });
|
||||
remove();
|
||||
cmdHelp();
|
||||
})) :
|
||||
dom.div(style({ marginTop: '2ex' }), 'Shortcut keys for mouse operation are currently not shown. ', dom.clickbutton('Enable', function click() {
|
||||
settingsPut({ ...settings, showShortcuts: true });
|
||||
remove();
|
||||
cmdHelp();
|
||||
})), dom.div(style({ marginTop: '2ex' }), 'To start composing a message when opening a "mailto:" link, register this application with your browser/system. ', dom.clickbutton('Register', attr.title('In most browsers, registering is only allowed on HTTPS URLs. Your browser may ask for confirmation. If nothing appears to happen, the registration may already have been present.'), function click() {
|
||||
if (!window.navigator.registerProtocolHandler) {
|
||||
window.alert('Registering a protocol handler ("mailto:") is not supported by your browser.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.navigator.registerProtocolHandler('mailto', '#compose %s');
|
||||
}
|
||||
catch (err) {
|
||||
window.alert('Error registering "mailto:" protocol handler: ' + errmsg(err));
|
||||
}
|
||||
}), ' ', dom.clickbutton('Unregister', attr.title('Not all browsers implement unregistering via JavaScript.'), function click() {
|
||||
// Not supported on firefox at the time of writing, and the signature is not in the types.
|
||||
if (!window.navigator.unregisterProtocolHandler) {
|
||||
window.alert('Unregistering a protocol handler ("mailto:") via JavaScript is not supported by your browser. See your browser settings to unregister.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.navigator.unregisterProtocolHandler('mailto', '#compose %s');
|
||||
}
|
||||
catch (err) {
|
||||
window.alert('Error unregistering "mailto:" protocol handler: ' + errmsg(err));
|
||||
return;
|
||||
}
|
||||
window.alert('"mailto:" protocol handler unregistered.');
|
||||
})), dom.div(style({ marginTop: '2ex' }), 'Mox is open source email server software, this is version ', moxversion, ', see ', dom.a(attr.href('licenses.txt'), 'licenses'), '.', dom.br(), 'Feedback, including bug reports, is appreciated! ', link('https://github.com/mjl-/mox/issues/new')))));
|
||||
].map(t => dom.tr(dom.td(t[0]), dom.td(t[1])))), dom.div(style({ marginTop: '2ex', marginBottom: '1ex' }), dom.span('Underdotted text', attr.title('Underdotted text shows additional information on hover.')), ' show an explanation or additional information when hovered.'), dom.div(style({ marginBottom: '1ex' }), 'Multiple messages can be selected by clicking messages while holding the control and/or shift keys. Dragging messages and dropping them on a mailbox moves the messages to that mailbox.'), dom.div(style({ marginBottom: '1ex' }), 'Text that changes ', dom.span(attr.title('Unicode blocks, e.g. from basic latin to cyrillic, or to emoticons.'), '"character groups"'), ' without whitespace has an ', dom.span(dom._class('scriptswitch'), 'orange underline'), ', which can be a sign of an intent to mislead (e.g. phishing).'), dom.div(style({ marginTop: '2ex' }), 'Mox is open source email server software, this is version ', moxversion, ', see ', dom.a(attr.href('licenses.txt'), 'licenses'), '.', dom.br(), 'Feedback, including bug reports, is appreciated! ', link('https://github.com/mjl-/mox/issues/new')))));
|
||||
};
|
||||
// Show tooltips for either the focused element, or otherwise for all elements
|
||||
// that aren't reachable with tabindex and aren't marked specially to prevent
|
||||
|
@ -3893,7 +3873,7 @@ const newMsgView = (miv, msglistView, listMailboxes, possibleLabels, messageLoad
|
|||
v: cmdViewAttachments,
|
||||
t: cmdShowText,
|
||||
T: cmdShowHTMLCycle,
|
||||
'ctrl I': cmdToggleHeaders,
|
||||
'ctrl i': cmdToggleHeaders,
|
||||
'alt j': cmdDown,
|
||||
'alt k': cmdUp,
|
||||
'alt ArrowDown': cmdDown,
|
||||
|
@ -3944,7 +3924,7 @@ const newMsgView = (miv, msglistView, listMailboxes, possibleLabels, messageLoad
|
|||
})));
|
||||
};
|
||||
loadButtons(parsedMessageOpt || null);
|
||||
loadMsgheaderView(msgheaderElem, miv.messageitem, settings.showHeaders, refineKeyword, false);
|
||||
loadMsgheaderView(msgheaderElem, miv.messageitem, accountSettings.ShowHeaders || [], refineKeyword, false);
|
||||
const headerTextMildStyle = css('headerTextMild', { textAlign: 'right', color: styles.colorMild });
|
||||
const loadHeaderDetails = (pm) => {
|
||||
if (msgheaderdetailsElem) {
|
||||
|
@ -4077,13 +4057,14 @@ const newMsgView = (miv, msglistView, listMailboxes, possibleLabels, messageLoad
|
|||
renderAttachments(); // Rerender opaciy on inline images.
|
||||
};
|
||||
const loadMoreHeaders = (pm) => {
|
||||
if (settings.showHeaders.length === 0) {
|
||||
const hl = accountSettings.ShowHeaders || [];
|
||||
if (hl.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < settings.showHeaders.length; i++) {
|
||||
for (let i = 0; i < hl.length; i++) {
|
||||
msgheaderElem.children[msgheaderElem.children.length - 1].remove();
|
||||
}
|
||||
settings.showHeaders.forEach(k => {
|
||||
hl.forEach(k => {
|
||||
const vl = pm.Headers?.[k];
|
||||
if (!vl || vl.length === 0) {
|
||||
return;
|
||||
|
@ -4102,7 +4083,7 @@ const newMsgView = (miv, msglistView, listMailboxes, possibleLabels, messageLoad
|
|||
updateKeywords: async (modseq, keywords) => {
|
||||
mi.Message.ModSeq = modseq;
|
||||
mi.Message.Keywords = keywords;
|
||||
loadMsgheaderView(msgheaderElem, miv.messageitem, settings.showHeaders, refineKeyword, false);
|
||||
loadMsgheaderView(msgheaderElem, miv.messageitem, accountSettings.ShowHeaders || [], refineKeyword, false);
|
||||
loadMoreHeaders(await parsedMessagePromise);
|
||||
},
|
||||
};
|
||||
|
@ -5380,6 +5361,11 @@ const newMsglistView = (msgElem, activeMailbox, listMailboxes, setLocationHash,
|
|||
moveclick(i + 1, e.key === 'J');
|
||||
}
|
||||
else if (e.key === 'PageUp' || e.key === 'h' || e.key === 'H' || e.key === 'PageDown' || e.key === 'l' || e.key === 'L') {
|
||||
// Commonly bound to "focus to browser address bar", moving cursor to one page down
|
||||
// without opening isn't useful enough.
|
||||
if (e.key === 'l' && e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
if (msgitemViews.length > 0) {
|
||||
let n = Math.max(1, Math.floor(scrollElemHeight() / mlv.itemHeight()) - 1);
|
||||
if (e.key === 'PageUp' || e.key === 'h' || e.key === 'H') {
|
||||
|
@ -5423,6 +5409,11 @@ const newMsglistView = (msgElem, activeMailbox, listMailboxes, setLocationHash,
|
|||
}
|
||||
}
|
||||
else if (e.key === 'u' || e.key === 'U') {
|
||||
// Commonly bound to "view source", moving cursor to next unread message without
|
||||
// opening isn't useful enough.
|
||||
if (e.key === 'u' && e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
for (i = i < 0 ? 0 : i + 1; i < msgitemViews.length; i += 1) {
|
||||
if (!msgitemViews[i].messageitem.Message.Seen || msgitemViews[i].collapsed && msgitemViews[i].findDescendant(miv => !miv.messageitem.Message.Seen)) {
|
||||
moveclick(i, true);
|
||||
|
|
|
@ -50,10 +50,6 @@ To simulate slow API calls and SSE events:
|
|||
|
||||
localStorage.setItem('sherpats-debug', JSON.stringify({waitMinMsec: 2000, waitMaxMsec: 4000}))
|
||||
|
||||
Show additional headers of messages:
|
||||
|
||||
settingsPut({...settings, showHeaders: ['Delivered-To', 'User-Agent', 'X-Mailer', 'Message-Id', 'List-Id', 'List-Post', 'X-Mox-Reason', 'TLS-Required']})
|
||||
|
||||
Enable logging and reload afterwards:
|
||||
|
||||
localStorage.setItem('log', 'yes')
|
||||
|
@ -147,7 +143,6 @@ try {
|
|||
let accountSettings: api.Settings
|
||||
|
||||
const defaultSettings = {
|
||||
showShortcuts: true, // Whether to briefly show shortcuts in bottom left when a button is clicked that has a keyboard shortcut.
|
||||
mailboxesWidth: 240,
|
||||
layout: 'auto', // Automatic switching between left/right and top/bottom layout, based on screen width.
|
||||
leftWidthPct: 50, // Split in percentage of remaining width for left/right layout.
|
||||
|
@ -160,7 +155,6 @@ const defaultSettings = {
|
|||
ignoreErrorsUntil: 0, // For unhandled javascript errors/rejected promises, we normally show a popup for details, but users can ignore them for a week at a time.
|
||||
mailboxCollapsed: {} as {[mailboxID: number]: boolean}, // Mailboxes that are collapsed.
|
||||
showAllHeaders: false, // Whether to show all message headers.
|
||||
showHeaders: [] as string[], // Additional message headers to show.
|
||||
threading: api.ThreadMode.ThreadOn,
|
||||
checkConsistency: location.hostname === 'localhost', // Enable UI update consistency checks, default only for local development.
|
||||
composeWidth: 0,
|
||||
|
@ -195,13 +189,6 @@ const parseSettings = (): typeof defaultSettings => {
|
|||
if (!mailboxCollapsed || typeof mailboxCollapsed !== 'object') {
|
||||
mailboxCollapsed = def.mailboxCollapsed
|
||||
}
|
||||
const getStringArray = (k: string): string[] => {
|
||||
const v = x[k]
|
||||
if (v && Array.isArray(v) && (v.length === 0 || typeof v[0] === 'string')) {
|
||||
return v
|
||||
}
|
||||
return def[k] as string[]
|
||||
}
|
||||
|
||||
return {
|
||||
refine: getString('refine'),
|
||||
|
@ -214,10 +201,8 @@ const parseSettings = (): typeof defaultSettings => {
|
|||
msglistfromPct: getInt('msglistfromPct'),
|
||||
ignoreErrorsUntil: getInt('ignoreErrorsUntil'),
|
||||
layout: getString('layout', 'auto', 'leftright', 'topbottom'),
|
||||
showShortcuts: getBool('showShortcuts'),
|
||||
mailboxCollapsed: mailboxCollapsed,
|
||||
showAllHeaders: getBool('showAllHeaders'),
|
||||
showHeaders: getStringArray('showHeaders'),
|
||||
threading: getString('threading', api.ThreadMode.ThreadOff, api.ThreadMode.ThreadOn, api.ThreadMode.ThreadUnread) as api.ThreadMode,
|
||||
checkConsistency: getBool('checkConsistency'),
|
||||
composeWidth: getInt('composeWidth'),
|
||||
|
@ -387,7 +372,7 @@ const envelopeIdentity = (l: api.MessageAddress[]): api.MessageAddress | null =>
|
|||
let shortcutElem = dom.div(css('shortcutFlash', {fontSize: '2em', position: 'absolute', left: '.25em', bottom: '.25em', backgroundColor: '#888', padding: '0.25em .5em', color: 'white', borderRadius: '.15em'}))
|
||||
let shortcutTimer = 0
|
||||
const showShortcut = (c: string) => {
|
||||
if (!settings.showShortcuts) {
|
||||
if (accountSettings?.NoShowShortcuts) {
|
||||
return
|
||||
}
|
||||
if (shortcutTimer) {
|
||||
|
@ -1119,6 +1104,8 @@ const cmdSettings = async () => {
|
|||
let quoting: HTMLSelectElement
|
||||
let showAddressSecurity: HTMLInputElement
|
||||
let showHTML: HTMLInputElement
|
||||
let showShortcuts: HTMLInputElement
|
||||
let showHeaders: HTMLTextAreaElement
|
||||
|
||||
if (!accountSettings) {
|
||||
throw new Error('No account settings fetched yet.')
|
||||
|
@ -1137,6 +1124,8 @@ const cmdSettings = async () => {
|
|||
Quoting: quoting.value as api.Quoting,
|
||||
ShowAddressSecurity: showAddressSecurity.checked,
|
||||
ShowHTML: showHTML.checked,
|
||||
NoShowShortcuts: !showShortcuts.checked,
|
||||
ShowHeaders: showHeaders.value.split('\n').map(s => s.trim()).filter(s => !!s),
|
||||
}
|
||||
await withDisabled(fieldset, client.SettingsSave(accSet))
|
||||
accountSettings = accSet
|
||||
|
@ -1171,8 +1160,61 @@ const cmdSettings = async () => {
|
|||
dom.label(
|
||||
style({margin: '1ex 0', display: 'block'}),
|
||||
showHTML=dom.input(attr.type('checkbox'), accountSettings.ShowHTML ? attr.checked('') : []),
|
||||
' Show HTML instead of text version by default',
|
||||
' Show email as HTML instead of text by default for first-time senders',
|
||||
attr.title('Whether to show HTML or text is remembered per sender. This sets the default for unknown correspondents.'),
|
||||
),
|
||||
|
||||
dom.label(
|
||||
style({margin: '1ex 0', display: 'block'}),
|
||||
showShortcuts=dom.input(attr.type('checkbox'), accountSettings.NoShowShortcuts ? [] : attr.checked('')),
|
||||
' Show shortcut keys in bottom left after interaction with mouse',
|
||||
),
|
||||
|
||||
dom.label(
|
||||
style({margin: '1ex 0', display: 'block'}),
|
||||
dom.div('Show additional headers'),
|
||||
showHeaders=dom.textarea(
|
||||
new String((accountSettings.ShowHeaders || []).join('\n')),
|
||||
style({width: '100%'}),
|
||||
attr.rows(''+Math.max(3, 1+(accountSettings.ShowHeaders || []).length)),
|
||||
),
|
||||
dom.div(style({fontStyle: 'italic'}), 'One header name per line, for example Delivered-To, X-Mox-Reason, User-Agent, ...'),
|
||||
),
|
||||
|
||||
|
||||
dom.div(
|
||||
style({marginTop: '2ex'}),
|
||||
'Register "mailto:" links with the browser/operating system to compose a message in webmail.',
|
||||
dom.br(),
|
||||
dom.clickbutton('Register', attr.title('In most browsers, registering is only allowed on HTTPS URLs. Your browser may ask for confirmation. If nothing appears to happen, the registration may already have been present.'), function click() {
|
||||
if (!window.navigator.registerProtocolHandler) {
|
||||
window.alert('Registering a protocol handler ("mailto:") is not supported by your browser.')
|
||||
return
|
||||
}
|
||||
try {
|
||||
window.navigator.registerProtocolHandler('mailto', '#compose %s')
|
||||
window.alert('"mailto:"-links have been registered')
|
||||
} catch (err) {
|
||||
window.alert('Error registering "mailto:" protocol handler: '+errmsg(err))
|
||||
}
|
||||
}),
|
||||
' ',
|
||||
dom.clickbutton('Unregister', attr.title('Not all browsers implement unregistering via JavaScript.'), function click() {
|
||||
// Not supported on firefox at the time of writing, and the signature is not in the types.
|
||||
if (!(window.navigator as any).unregisterProtocolHandler) {
|
||||
window.alert('Unregistering a protocol handler ("mailto:") via JavaScript is not supported by your browser. See your browser settings to unregister.')
|
||||
return
|
||||
}
|
||||
try {
|
||||
(window.navigator as any).unregisterProtocolHandler('mailto', '#compose %s')
|
||||
} catch (err) {
|
||||
window.alert('Error unregistering "mailto:" protocol handler: '+errmsg(err))
|
||||
return
|
||||
}
|
||||
window.alert('"mailto:" protocol handler unregistered.')
|
||||
}),
|
||||
),
|
||||
|
||||
dom.br(),
|
||||
dom.div(
|
||||
dom.submitbutton('Save'),
|
||||
|
@ -1184,7 +1226,7 @@ const cmdSettings = async () => {
|
|||
|
||||
// Show help popup, with shortcuts and basic explanation.
|
||||
const cmdHelp = async () => {
|
||||
const remove = popup(
|
||||
popup(
|
||||
css('popupHelp', {padding: '1em 1em 2em 1em'}),
|
||||
dom.h1('Help and keyboard shortcuts'),
|
||||
dom.div(style({display: 'flex'}),
|
||||
|
@ -1212,7 +1254,12 @@ const cmdHelp = async () => {
|
|||
dom.tr(
|
||||
dom.td('↓', ', j'),
|
||||
dom.td('down one message'),
|
||||
dom.td(attr.rowspan('6'), css('helpSideNote', {color: '#888', borderLeft: '2px solid', borderLeftColor: '#888', paddingLeft: '.5em'}), 'hold ctrl to only move focus', dom.br(), 'hold shift to expand selection'),
|
||||
dom.td(
|
||||
attr.rowspan('6'),
|
||||
css('helpSideNote', {color: '#888', borderLeft: '2px solid', borderLeftColor: '#888', paddingLeft: '.5em'}),
|
||||
dom.div('hold ctrl to only move focus', attr.title('ctrl-l and ctrl-u are left for the browser the handle')),
|
||||
dom.div('hold shift to expand selection'),
|
||||
),
|
||||
),
|
||||
[
|
||||
[['↑', ', k'], 'up one message'],
|
||||
|
@ -1273,7 +1320,7 @@ const cmdHelp = async () => {
|
|||
['O', 'show raw message'],
|
||||
['ctrl p', 'print message'],
|
||||
['I', 'toggle internals'],
|
||||
['ctrl I', 'toggle all headers'],
|
||||
['ctrl i', 'toggle all headers'],
|
||||
|
||||
['alt k, alt ArrowUp', 'scroll up'],
|
||||
['alt j, alt ArrowDown', 'scroll down'],
|
||||
|
@ -1294,51 +1341,6 @@ const cmdHelp = async () => {
|
|||
dom.div(style({marginBottom: '1ex'}), 'Multiple messages can be selected by clicking messages while holding the control and/or shift keys. Dragging messages and dropping them on a mailbox moves the messages to that mailbox.'),
|
||||
dom.div(style({marginBottom: '1ex'}), 'Text that changes ', dom.span(attr.title('Unicode blocks, e.g. from basic latin to cyrillic, or to emoticons.'), '"character groups"'), ' without whitespace has an ', dom.span(dom._class('scriptswitch'), 'orange underline'), ', which can be a sign of an intent to mislead (e.g. phishing).'),
|
||||
|
||||
settings.showShortcuts ?
|
||||
dom.div(style({marginTop: '2ex'}), 'Shortcut keys for mouse operation are shown in the bottom left. ',
|
||||
dom.clickbutton('Disable', function click() {
|
||||
settingsPut({...settings, showShortcuts: false})
|
||||
remove()
|
||||
cmdHelp()
|
||||
})
|
||||
) :
|
||||
dom.div(style({marginTop: '2ex'}), 'Shortcut keys for mouse operation are currently not shown. ',
|
||||
dom.clickbutton('Enable', function click() {
|
||||
settingsPut({...settings, showShortcuts: true})
|
||||
remove()
|
||||
cmdHelp()
|
||||
})
|
||||
),
|
||||
dom.div(
|
||||
style({marginTop: '2ex'}),
|
||||
'To start composing a message when opening a "mailto:" link, register this application with your browser/system. ',
|
||||
dom.clickbutton('Register', attr.title('In most browsers, registering is only allowed on HTTPS URLs. Your browser may ask for confirmation. If nothing appears to happen, the registration may already have been present.'), function click() {
|
||||
if (!window.navigator.registerProtocolHandler) {
|
||||
window.alert('Registering a protocol handler ("mailto:") is not supported by your browser.')
|
||||
return
|
||||
}
|
||||
try {
|
||||
window.navigator.registerProtocolHandler('mailto', '#compose %s')
|
||||
} catch (err) {
|
||||
window.alert('Error registering "mailto:" protocol handler: '+errmsg(err))
|
||||
}
|
||||
}),
|
||||
' ',
|
||||
dom.clickbutton('Unregister', attr.title('Not all browsers implement unregistering via JavaScript.'), function click() {
|
||||
// Not supported on firefox at the time of writing, and the signature is not in the types.
|
||||
if (!(window.navigator as any).unregisterProtocolHandler) {
|
||||
window.alert('Unregistering a protocol handler ("mailto:") via JavaScript is not supported by your browser. See your browser settings to unregister.')
|
||||
return
|
||||
}
|
||||
try {
|
||||
(window.navigator as any).unregisterProtocolHandler('mailto', '#compose %s')
|
||||
} catch (err) {
|
||||
window.alert('Error unregistering "mailto:" protocol handler: '+errmsg(err))
|
||||
return
|
||||
}
|
||||
window.alert('"mailto:" protocol handler unregistered.')
|
||||
}),
|
||||
),
|
||||
dom.div(style({marginTop: '2ex'}), 'Mox is open source email server software, this is version ', moxversion, ', see ', dom.a(attr.href('licenses.txt'), 'licenses'), '.', dom.br(), 'Feedback, including bug reports, is appreciated! ', link('https://github.com/mjl-/mox/issues/new')),
|
||||
),
|
||||
),
|
||||
|
@ -3156,7 +3158,7 @@ const newMsgView = (miv: MsgitemView, msglistView: MsglistView, listMailboxes: l
|
|||
v: cmdViewAttachments,
|
||||
t: cmdShowText,
|
||||
T: cmdShowHTMLCycle,
|
||||
'ctrl I': cmdToggleHeaders,
|
||||
'ctrl i': cmdToggleHeaders,
|
||||
|
||||
'alt j': cmdDown,
|
||||
'alt k': cmdUp,
|
||||
|
@ -3251,7 +3253,7 @@ const newMsgView = (miv: MsgitemView, msglistView: MsglistView, listMailboxes: l
|
|||
}
|
||||
loadButtons(parsedMessageOpt || null)
|
||||
|
||||
loadMsgheaderView(msgheaderElem, miv.messageitem, settings.showHeaders, refineKeyword, false)
|
||||
loadMsgheaderView(msgheaderElem, miv.messageitem, accountSettings.ShowHeaders || [], refineKeyword, false)
|
||||
|
||||
const headerTextMildStyle = css('headerTextMild', {textAlign: 'right', color: styles.colorMild})
|
||||
|
||||
|
@ -3510,13 +3512,14 @@ const newMsgView = (miv: MsgitemView, msglistView: MsglistView, listMailboxes: l
|
|||
}
|
||||
|
||||
const loadMoreHeaders = (pm: api.ParsedMessage) => {
|
||||
if (settings.showHeaders.length === 0) {
|
||||
const hl = accountSettings.ShowHeaders || []
|
||||
if (hl.length === 0) {
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < settings.showHeaders.length; i++) {
|
||||
for (let i = 0; i < hl.length; i++) {
|
||||
msgheaderElem.children[msgheaderElem.children.length-1].remove()
|
||||
}
|
||||
settings.showHeaders.forEach(k => {
|
||||
hl.forEach(k => {
|
||||
const vl = pm.Headers?.[k]
|
||||
if (!vl || vl.length === 0) {
|
||||
return
|
||||
|
@ -3539,7 +3542,7 @@ const newMsgView = (miv: MsgitemView, msglistView: MsglistView, listMailboxes: l
|
|||
updateKeywords: async (modseq: number, keywords: string[]) => {
|
||||
mi.Message.ModSeq = modseq
|
||||
mi.Message.Keywords = keywords
|
||||
loadMsgheaderView(msgheaderElem, miv.messageitem, settings.showHeaders, refineKeyword, false)
|
||||
loadMsgheaderView(msgheaderElem, miv.messageitem, accountSettings.ShowHeaders || [], refineKeyword, false)
|
||||
loadMoreHeaders(await parsedMessagePromise)
|
||||
},
|
||||
}
|
||||
|
@ -4981,6 +4984,12 @@ const newMsglistView = (msgElem: HTMLElement, activeMailbox: () => api.Mailbox |
|
|||
} else if (e.key === 'ArrowDown' || e.key === 'j' || e.key === 'J') {
|
||||
moveclick(i+1, e.key === 'J')
|
||||
} else if (e.key === 'PageUp' || e.key === 'h' || e.key === 'H' || e.key === 'PageDown' || e.key === 'l' || e.key === 'L') {
|
||||
// Commonly bound to "focus to browser address bar", moving cursor to one page down
|
||||
// without opening isn't useful enough.
|
||||
if (e.key === 'l' && e.ctrlKey) {
|
||||
return
|
||||
}
|
||||
|
||||
if (msgitemViews.length > 0) {
|
||||
let n = Math.max(1, Math.floor(scrollElemHeight()/mlv.itemHeight())-1)
|
||||
if (e.key === 'PageUp' || e.key === 'h' || e.key === 'H') {
|
||||
|
@ -5017,6 +5026,12 @@ const newMsglistView = (msgElem: HTMLElement, activeMailbox: () => api.Mailbox |
|
|||
moveclick(msgitemViews.indexOf(thrmiv), true)
|
||||
}
|
||||
} else if (e.key === 'u' || e.key === 'U') {
|
||||
// Commonly bound to "view source", moving cursor to next unread message without
|
||||
// opening isn't useful enough.
|
||||
if (e.key === 'u' && e.ctrlKey) {
|
||||
return
|
||||
}
|
||||
|
||||
for (i = i < 0 ? 0 : i+1; i < msgitemViews.length; i += 1) {
|
||||
if (!msgitemViews[i].messageitem.Message.Seen || msgitemViews[i].collapsed && msgitemViews[i].findDescendant(miv => !miv.messageitem.Message.Seen)) {
|
||||
moveclick(i, true)
|
||||
|
|
Loading…
Reference in a new issue