2023-01-30 16:27:06 +03:00
package main
import (
"context"
cryptorand "crypto/rand"
"fmt"
"net"
"os"
"os/signal"
"path/filepath"
"runtime/debug"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/dnsbl"
"github.com/mjl-/mox/message"
"github.com/mjl-/mox/metrics"
"github.com/mjl-/mox/mlog"
"github.com/mjl-/mox/mox-"
"github.com/mjl-/mox/moxio"
"github.com/mjl-/mox/moxvar"
"github.com/mjl-/mox/store"
"github.com/mjl-/mox/updates"
)
func monitorDNSBL ( log * mlog . Log ) {
defer func ( ) {
// On error, don't bring down the entire server.
x := recover ( )
if x != nil {
log . Error ( "monitordnsbl panic" , mlog . Field ( "panic" , x ) )
debug . PrintStack ( )
metrics . PanicInc ( "serve" )
}
} ( )
l , ok := mox . Conf . Static . Listeners [ "public" ]
if ! ok {
log . Info ( "no listener named public, not monitoring our ips at dnsbls" )
return
}
var zones [ ] dns . Domain
for _ , zone := range l . SMTP . DNSBLs {
d , err := dns . ParseDomain ( zone )
if err != nil {
log . Fatalx ( "parsing dnsbls zone" , err , mlog . Field ( "zone" , zone ) )
}
zones = append ( zones , d )
}
if len ( zones ) == 0 {
return
}
type key struct {
zone dns . Domain
ip string
}
metrics := map [ key ] prometheus . GaugeFunc { }
var statusMutex sync . Mutex
statuses := map [ key ] bool { }
resolver := dns . StrictResolver { Pkg : "dnsblmonitor" }
var sleep time . Duration // No sleep on first iteration.
for {
time . Sleep ( sleep )
sleep = 3 * time . Hour
ips , err := mox . IPs ( mox . Context )
if err != nil {
log . Errorx ( "listing ips for dnsbl monitor" , err )
continue
}
for _ , ip := range ips {
if ip . IsLoopback ( ) || ip . IsPrivate ( ) {
continue
}
for _ , zone := range zones {
status , expl , err := dnsbl . Lookup ( mox . Context , resolver , zone , ip )
if err != nil {
log . Errorx ( "dnsbl monitor lookup" , err , mlog . Field ( "ip" , ip ) , mlog . Field ( "zone" , zone ) , mlog . Field ( "expl" , expl ) , mlog . Field ( "status" , status ) )
}
k := key { zone , ip . String ( ) }
statusMutex . Lock ( )
statuses [ k ] = status == dnsbl . StatusPass
statusMutex . Unlock ( )
if _ , ok := metrics [ k ] ; ! ok {
metrics [ k ] = promauto . NewGaugeFunc (
prometheus . GaugeOpts {
Name : "mox_dnsbl_ips_success" ,
Help : "DNSBL lookups to configured DNSBLs of our IPs." ,
ConstLabels : prometheus . Labels {
"zone" : zone . String ( ) ,
"ip" : k . ip ,
} ,
} ,
func ( ) float64 {
statusMutex . Lock ( )
defer statusMutex . Unlock ( )
if statuses [ k ] {
return 1
}
return 0
} ,
)
}
time . Sleep ( time . Second )
}
}
}
}
func cmdServe ( c * cmd ) {
c . help = ` Start mox , serving SMTP / IMAP / HTTPS .
Incoming email is accepted over SMTP . Email can be retrieved by users using
IMAP . HTTP listeners are started for the admin / account web interfaces , and for
automated TLS configuration . Missing essential TLS certificates are immediately
requested , other TLS certificates are requested on demand .
`
args := c . Parse ( )
if len ( args ) != 0 {
c . Usage ( )
}
mox . MustLoadConfig ( )
mlog . Logfmt = true
log := mlog . New ( "serve" )
if os . Getuid ( ) == 0 {
log . Fatal ( "refusing to run as root, please start mox as unprivileged user" )
}
if fds := os . Getenv ( "MOX_RESTART_CTL_SOCKET" ) ; fds != "" {
log . Print ( "restarted" )
fd , err := strconv . ParseUint ( fds , 10 , 32 )
if err != nil {
log . Fatalx ( "restart with invalid ctl socket" , err , mlog . Field ( "fd" , fds ) )
}
f := os . NewFile ( uintptr ( fd ) , "restartctl" )
if _ , err := fmt . Fprint ( f , "ok\n" ) ; err != nil {
log . Infox ( "writing ok to restart ctl socket" , err )
}
2023-02-16 15:22:00 +03:00
err = f . Close ( )
log . Check ( err , "closing restart ctl socket" )
2023-01-30 16:27:06 +03:00
}
log . Print ( "starting up" , mlog . Field ( "version" , moxvar . Version ) )
shutdown := func ( ) {
2023-02-16 11:57:27 +03:00
// We indicate we are shutting down. Causes new connections and new SMTP commands
// to be rejected. Should stop active connections pretty quickly.
mox . ShutdownCancel ( )
2023-01-30 16:27:06 +03:00
// Now we are going to wait for all connections to be gone, up to a timeout.
done := mox . Connections . Done ( )
2023-02-16 11:57:27 +03:00
second := time . Tick ( time . Second )
2023-01-30 16:27:06 +03:00
select {
case <- done :
2023-02-16 11:57:27 +03:00
log . Print ( "connections shutdown, waiting until 1 second passed" )
<- second
2023-01-30 16:27:06 +03:00
case <- time . Tick ( 3 * time . Second ) :
2023-02-16 11:57:27 +03:00
// We now cancel all pending operations, and set an immediate deadline on sockets.
// Should get us a clean shutdown relatively quickly.
mox . ContextCancel ( )
2023-01-30 16:27:06 +03:00
mox . Connections . Shutdown ( )
2023-02-16 11:57:27 +03:00
second := time . Tick ( time . Second )
2023-01-30 16:27:06 +03:00
select {
case <- done :
2023-02-16 11:57:27 +03:00
log . Print ( "no more connections, shutdown is clean, waiting until 1 second passed" )
<- second // Still wait for second, giving processes like imports a chance to clean up.
case <- second :
2023-01-30 16:27:06 +03:00
log . Print ( "shutting down with pending sockets" )
}
}
2023-02-16 15:22:00 +03:00
err := os . Remove ( mox . DataDirPath ( "ctl" ) )
log . Check ( err , "removing ctl unix domain socket during shutdown" )
2023-01-30 16:27:06 +03:00
}
if err := moxio . CheckUmask ( ) ; err != nil {
log . Errorx ( "bad umask" , err )
}
if mox . Conf . Static . CheckUpdates {
2023-01-31 02:16:01 +03:00
checkUpdates := func ( ) time . Duration {
next := 24 * time . Hour
2023-01-30 16:27:06 +03:00
current , lastknown , mtime , err := mox . LastKnown ( )
if err != nil {
2023-01-31 02:16:01 +03:00
log . Infox ( "determining own version before checking for updates, trying again in 24h" , err )
return next
2023-01-30 16:27:06 +03:00
}
2023-01-31 02:16:01 +03:00
// We don't want to check for updates at every startup. So we sleep based on file
// mtime. But file won't exist initially.
2023-01-30 16:27:06 +03:00
if ! mtime . IsZero ( ) && time . Since ( mtime ) < 24 * time . Hour {
2023-01-31 02:16:01 +03:00
d := 24 * time . Hour - time . Since ( mtime )
log . Debug ( "sleeping for next check for updates" , mlog . Field ( "sleep" , d ) )
time . Sleep ( d )
next = 0
2023-01-30 16:27:06 +03:00
}
now := time . Now ( )
if err := os . Chtimes ( mox . DataDirPath ( "lastknownversion" ) , now , now ) ; err != nil {
2023-01-31 02:16:01 +03:00
if ! os . IsNotExist ( err ) {
log . Infox ( "setting mtime on lastknownversion file, continuing" , err )
}
2023-01-30 16:27:06 +03:00
}
2023-01-31 02:16:01 +03:00
2023-01-30 16:27:06 +03:00
log . Debug ( "checking for updates" , mlog . Field ( "lastknown" , lastknown ) )
updatesctx , updatescancel := context . WithTimeout ( mox . Context , time . Minute )
latest , _ , changelog , err := updates . Check ( updatesctx , dns . StrictResolver { } , dns . Domain { ASCII : changelogDomain } , lastknown , changelogURL , changelogPubKey )
updatescancel ( )
if err != nil {
log . Infox ( "checking for updates" , err , mlog . Field ( "latest" , latest ) )
2023-01-31 02:16:01 +03:00
return next
2023-01-30 16:27:06 +03:00
}
if ! latest . After ( lastknown ) {
log . Debug ( "no new version available" )
2023-01-31 02:16:01 +03:00
return next
2023-01-30 16:27:06 +03:00
}
if len ( changelog . Changes ) == 0 {
log . Info ( "new version available, but changelog is empty, ignoring" , mlog . Field ( "latest" , latest ) )
2023-01-31 02:16:01 +03:00
return next
2023-01-30 16:27:06 +03:00
}
var cl string
for i := len ( changelog . Changes ) - 1 ; i >= 0 ; i -- {
cl += changelog . Changes [ i ] . Text + "\n\n"
}
a , err := store . OpenAccount ( mox . Conf . Static . Postmaster . Account )
if err != nil {
log . Infox ( "open account for postmaster changelog delivery" , err )
2023-01-31 02:16:01 +03:00
return next
2023-01-30 16:27:06 +03:00
}
2023-02-16 15:22:00 +03:00
defer func ( ) {
err := a . Close ( )
log . Check ( err , "closing account" )
} ( )
2023-01-30 16:27:06 +03:00
f , err := store . CreateMessageTemp ( "changelog" )
if err != nil {
log . Infox ( "making temporary message file for changelog delivery" , err )
2023-01-31 02:16:01 +03:00
return next
2023-01-30 16:27:06 +03:00
}
m := & store . Message { Received : time . Now ( ) , Flags : store . Flags { Flagged : true } }
n , err := fmt . Fprintf ( f , "Date: %s\r\nSubject: mox update %s available, changelog\r\n\r\nHi!\r\n\r\nVersion %s of mox is available.\r\nThe changes compared to the previous update notification email:\r\n\r\n%s\r\n\r\nDon't forget to update, this install is at %s.\r\nPlease report any issues at https://github.com/mjl-/mox\r\n" , time . Now ( ) . Format ( message . RFC5322Z ) , latest , latest , strings . ReplaceAll ( cl , "\n" , "\r\n" ) , current )
if err != nil {
log . Infox ( "writing temporary message file for changelog delivery" , err )
2023-01-31 02:16:01 +03:00
return next
2023-01-30 16:27:06 +03:00
}
m . Size = int64 ( n )
if err := a . DeliverMailbox ( log , mox . Conf . Static . Postmaster . Mailbox , m , f , true ) ; err != nil {
log . Infox ( "changelog delivery" , err )
2023-02-16 15:22:00 +03:00
err := os . Remove ( f . Name ( ) )
log . Check ( err , "removing temporary changelog message after delivery failure" )
2023-01-30 16:27:06 +03:00
}
log . Info ( "delivered changelog" , mlog . Field ( "current" , current ) , mlog . Field ( "lastknown" , lastknown ) , mlog . Field ( "latest" , latest ) )
if err := mox . StoreLastKnown ( latest ) ; err != nil {
// This will be awkward, we'll keep notifying the postmaster once every 24h...
log . Infox ( "updating last known version" , err )
}
2023-01-31 02:16:01 +03:00
return next
2023-01-30 16:27:06 +03:00
}
go func ( ) {
for {
2023-01-31 02:16:01 +03:00
next := checkUpdates ( )
time . Sleep ( next )
2023-01-30 16:27:06 +03:00
}
} ( )
}
// Initialize key and random buffer for creating opaque SMTP
// transaction IDs based on "cid"s.
recvidpath := mox . DataDirPath ( "receivedid.key" )
recvidbuf , err := os . ReadFile ( recvidpath )
if err != nil || len ( recvidbuf ) != 16 + 8 {
recvidbuf = make ( [ ] byte , 16 + 8 )
if _ , err := cryptorand . Read ( recvidbuf ) ; err != nil {
log . Fatalx ( "reading random recvid data" , err )
}
if err := os . WriteFile ( recvidpath , recvidbuf , 0660 ) ; err != nil {
log . Fatalx ( "writing recvidpath" , err , mlog . Field ( "path" , recvidpath ) )
}
}
if err := mox . ReceivedIDInit ( recvidbuf [ : 16 ] , recvidbuf [ 16 : ] ) ; err != nil {
log . Fatalx ( "init receivedid" , err )
}
// We start the network listeners first. If an instance is already running, we'll
// get errors about address being in use. We listen to the unix domain socket
// afterwards, which we always remove before listening. We need to do that because
// we may not have cleaned up our control socket during unexpected shutdown. We
// don't want to remove and listen on the unix domain socket first. If we would, we
// would make the existing instance unreachable over its ctl socket, and then fail
// because the network addresses are taken.
mtastsdbRefresher := true
if err := start ( mtastsdbRefresher ) ; err != nil {
log . Fatalx ( "start" , err )
}
go monitorDNSBL ( log )
ctlpath := mox . DataDirPath ( "ctl" )
2023-02-16 15:22:00 +03:00
_ = os . Remove ( ctlpath )
2023-01-30 16:27:06 +03:00
ctl , err := net . Listen ( "unix" , ctlpath )
if err != nil {
log . Fatalx ( "listen on ctl unix domain socket" , err )
}
go func ( ) {
for {
conn , err := ctl . Accept ( )
if err != nil {
log . Printx ( "accept for ctl" , err )
continue
}
cid := mox . Cid ( )
ctx := context . WithValue ( mox . Context , mlog . CidKey , cid )
go servectl ( ctx , log . WithCid ( cid ) , conn , shutdown )
}
} ( )
// Remove old temporary files that somehow haven't been cleaned up.
tmpdir := mox . DataDirPath ( "tmp" )
os . MkdirAll ( tmpdir , 0770 )
tmps , err := os . ReadDir ( tmpdir )
if err != nil {
log . Errorx ( "listing files in tmpdir" , err )
} else {
now := time . Now ( )
for _ , e := range tmps {
if fi , err := e . Info ( ) ; err != nil {
log . Errorx ( "stat tmp file" , err , mlog . Field ( "filename" , e . Name ( ) ) )
} else if now . Sub ( fi . ModTime ( ) ) > 7 * 24 * time . Hour {
p := filepath . Join ( tmpdir , e . Name ( ) )
if err := os . Remove ( p ) ; err != nil {
log . Errorx ( "removing stale temporary file" , err , mlog . Field ( "path" , p ) )
} else {
log . Info ( "removed stale temporary file" , mlog . Field ( "path" , p ) )
}
}
}
}
// Graceful shutdown.
sigc := make ( chan os . Signal , 1 )
signal . Notify ( sigc , os . Interrupt , syscall . SIGTERM )
sig := <- sigc
log . Print ( "shutting down, waiting max 3s for existing connections" , mlog . Field ( "signal" , sig ) )
shutdown ( )
}