ygggo/cmd/yggmail/main.go

252 lines
6.8 KiB
Go
Raw Normal View History

2021-07-18 23:03:28 +01:00
/*
* Copyright (c) 2021 Neil Alexander
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
2021-07-07 18:15:07 +01:00
package main
import (
"bytes"
"crypto/ed25519"
"encoding/hex"
"flag"
"fmt"
"log"
"os"
"os/signal"
2021-07-07 18:15:07 +01:00
"strings"
"syscall"
2021-07-07 18:15:07 +01:00
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/fatih/color"
2021-07-07 18:15:07 +01:00
"golang.org/x/term"
"github.com/neilalexander/yggmail/internal/config"
"github.com/neilalexander/yggmail/internal/imapserver"
"github.com/neilalexander/yggmail/internal/smtpsender"
"github.com/neilalexander/yggmail/internal/smtpserver"
"github.com/neilalexander/yggmail/internal/storage/sqlite3"
"github.com/neilalexander/yggmail/internal/transport"
2021-07-08 23:12:20 +01:00
"github.com/neilalexander/yggmail/internal/utils"
"golang.org/x/crypto/bcrypt"
2021-07-07 18:15:07 +01:00
)
type peerAddrList []string
func (i *peerAddrList) String() string {
return strings.Join(*i, ", ")
}
func (i *peerAddrList) Set(value string) error {
*i = append(*i, value)
return nil
}
2021-07-07 18:15:07 +01:00
func main() {
2021-07-10 13:29:56 +01:00
rawlog := log.New(color.Output, "", 0)
green := color.New(color.FgGreen).SprintfFunc()
2022-09-04 20:51:44 +01:00
log := log.New(rawlog.Writer(), fmt.Sprintf("[ %s ] ", green("Yggmail")), log.LstdFlags|log.Lmsgprefix)
2021-07-07 18:15:07 +01:00
var peerAddrs peerAddrList
database := flag.String("database", "yggmail.db", "SQLite database file")
smtpaddr := flag.String("smtp", "localhost:1025", "SMTP listen address")
imapaddr := flag.String("imap", "localhost:1143", "IMAP listen address")
multicast := flag.Bool("multicast", false, "Connect to Yggdrasil peers on your LAN")
2025-12-20 14:06:14 +00:00
mcastregexp := flag.String("mcastregexp", ".*", "Regexp for multicast")
password := flag.Bool("password", false, "Set a new IMAP/SMTP password")
passwordhash := flag.String("passwordhash", "", "Set a new IMAP/SMTP password (hash)")
flag.Var(&peerAddrs, "peer", "Connect to a specific Yggdrasil static peer (this option can be given more than once)")
2021-07-09 18:42:52 +01:00
flag.Parse()
2021-07-09 18:42:52 +01:00
if flag.NFlag() == 0 {
fmt.Println("Yggmail must be started with either one or more Yggdrasil peers")
2021-07-09 18:42:52 +01:00
fmt.Println("specified, multicast enabled, or both.")
fmt.Println()
fmt.Println("Available options:")
fmt.Println()
flag.PrintDefaults()
os.Exit(0)
}
2021-07-07 18:15:07 +01:00
storage, err := sqlite3.NewSQLite3StorageStorage(*database)
if err != nil {
panic(err)
}
2025-12-20 14:06:14 +00:00
defer storage.Close() // nolint:errcheck
2021-07-07 18:15:07 +01:00
log.Printf("Using database file %q\n", *database)
skStr, err := storage.ConfigGet("private_key")
if err != nil {
panic(err)
}
sk := make(ed25519.PrivateKey, ed25519.PrivateKeySize)
if skStr == "" {
if _, sk, err = ed25519.GenerateKey(nil); err != nil {
panic(err)
}
if err := storage.ConfigSet("private_key", hex.EncodeToString(sk)); err != nil {
panic(err)
}
log.Printf("Generated new server identity")
} else {
skBytes, err := hex.DecodeString(skStr)
if err != nil {
panic(err)
}
copy(sk, skBytes)
}
pk := sk.Public().(ed25519.PublicKey)
2021-07-09 00:26:43 +01:00
log.Printf("Mail address: %s@%s\n", hex.EncodeToString(pk), utils.Domain)
2021-07-07 18:15:07 +01:00
for _, name := range []string{"INBOX", "Outbox"} {
if err := storage.MailboxCreate(name); err != nil {
panic(err)
}
}
2021-07-07 18:15:07 +01:00
switch {
2021-07-09 00:08:26 +01:00
case password != nil && *password:
log.Println("Please enter your new password:")
password1, err := term.ReadPassword(int(os.Stdin.Fd()))
2021-07-07 18:15:07 +01:00
if err != nil {
panic(err)
}
fmt.Println()
2021-07-09 00:08:26 +01:00
log.Println("Please enter your new password again:")
password2, err := term.ReadPassword(int(os.Stdin.Fd()))
2021-07-07 18:15:07 +01:00
if err != nil {
panic(err)
}
fmt.Println()
if !bytes.Equal(password1, password2) {
2021-07-09 00:08:26 +01:00
log.Println("The supplied passwords do not match")
2021-07-07 18:15:07 +01:00
os.Exit(1)
}
2025-12-20 14:06:14 +00:00
// trim away whitespace of UTF-8 bytes now as string
finalPassword := strings.TrimSpace(string(password1))
// perform hash
hash, err := bcrypt.GenerateFromPassword([]byte(finalPassword), bcrypt.DefaultCost)
if err != nil {
log.Printf("bcrypt.GenerateFromPassword: %v\n", err)
os.Exit(1)
} else if err := storage.ConfigSetPassword(string(hash)); err != nil {
2021-07-09 00:08:26 +01:00
log.Println("Failed to set password:", err)
2021-07-07 18:15:07 +01:00
os.Exit(1)
}
2021-07-09 00:08:26 +01:00
log.Println("Password for IMAP and SMTP has been updated!")
2021-07-07 18:15:07 +01:00
os.Exit(0)
2021-07-09 18:42:52 +01:00
case passwordhash != nil && *passwordhash != "":
2025-12-20 14:06:14 +00:00
var hash = strings.TrimSpace(*passwordhash)
if len(hash) == 0 {
2025-12-20 14:06:14 +00:00
log.Println("Password hash cannot be blank")
os.Exit(1)
}
2025-12-20 14:06:14 +00:00
log.Printf("Using password hash: '%v'\n", hash)
if _, err := bcrypt.Cost(([]byte)(hash)); err != nil {
2025-12-20 14:06:14 +00:00
log.Printf("The provided hash is invalid %v\n", err)
os.Exit(1)
} else if err := storage.ConfigSetPassword(hash); err != nil {
2025-12-20 14:06:14 +00:00
log.Println("Failed to set password: ", err)
os.Exit(1)
}
log.Println("Password for IMAP and SMTP has been updated!")
case (multicast == nil || !*multicast) && len(peerAddrs) == 0:
2021-07-09 18:42:52 +01:00
log.Printf("You must specify either -peer, -multicast or both!")
os.Exit(0)
2024-10-02 13:31:09 +03:00
2021-07-07 18:15:07 +01:00
}
cfg := &config.Config{
PublicKey: pk,
PrivateKey: sk,
}
2024-10-02 13:31:09 +03:00
transport, err := transport.NewYggdrasilTransport(rawlog, sk, pk, peerAddrs, *multicast, *mcastregexp)
2021-07-07 18:15:07 +01:00
if err != nil {
panic(err)
}
2021-07-09 23:43:09 +01:00
queues := smtpsender.NewQueues(cfg, log, transport, storage)
var notify *imapserver.IMAPNotify
2021-07-07 18:15:07 +01:00
imapBackend := &imapserver.Backend{
Log: log,
Config: cfg,
Storage: storage,
}
2021-07-07 18:15:07 +01:00
_, notify, err = imapserver.NewIMAPServer(imapBackend, *imapaddr, true)
if err != nil {
log.Fatal(err)
}
log.Println("Listening for IMAP on:", *imapaddr)
2021-07-07 18:15:07 +01:00
go func() {
localBackend := &smtpserver.Backend{
Log: log,
Mode: smtpserver.BackendModeInternal,
Config: cfg,
Storage: storage,
Queues: queues,
Notify: notify,
2021-07-07 18:15:07 +01:00
}
localServer := smtp.NewServer(localBackend)
localServer.Addr = *smtpaddr
2021-07-09 00:26:43 +01:00
localServer.Domain = hex.EncodeToString(pk)
2022-09-04 20:51:44 +01:00
localServer.MaxMessageBytes = 1024 * 1024 * 32
2021-07-07 18:15:07 +01:00
localServer.MaxRecipients = 50
localServer.AllowInsecureAuth = true
localServer.EnableAuth(sasl.Login, func(conn *smtp.Conn) sasl.Server {
return sasl.NewLoginServer(func(username, password string) error {
_, err := localBackend.Login(nil, username, password)
return err
})
})
log.Println("Listening for SMTP on:", localServer.Addr)
if err := localServer.ListenAndServe(); err != nil {
log.Fatal(err)
}
}()
go func() {
overlayBackend := &smtpserver.Backend{
Log: log,
Mode: smtpserver.BackendModeExternal,
Config: cfg,
Storage: storage,
Queues: queues,
Notify: notify,
2021-07-07 18:15:07 +01:00
}
overlayServer := smtp.NewServer(overlayBackend)
2021-07-09 00:26:43 +01:00
overlayServer.Domain = hex.EncodeToString(pk)
2022-09-04 20:51:44 +01:00
overlayServer.MaxMessageBytes = 1024 * 1024 * 32
2021-07-07 18:15:07 +01:00
overlayServer.MaxRecipients = 50
overlayServer.AuthDisabled = true
if err := overlayServer.Serve(transport.Listener()); err != nil {
2021-07-07 18:15:07 +01:00
log.Fatal(err)
}
}()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
log.Println("Shutting down")
2021-07-07 18:15:07 +01:00
}