ygggo/cmd/yggmail/main.go
Kaiy Belist 39759db7c2 - Inter-node Protocol: Implemented a custom communication protocol based on Protobuf structures.
- Bridge Node Support: Added functionality to manage mail (send/receive) on remote nodes via a bridge node. Full transparent management requires a private key on the bridge, while the remote node can operate with only a public key.
 - End-to-End Encryption (E2EE): Integrated E2EE using age. Encryption and decryption occur strictly at the bridge level; node administrators cannot access message content.
 - New Addressing Schema: Transitioned from nodepub@yggmail to userpub@nodepub.
 - Key Adaptation: Implemented ed25519 to x25519 coordinate adaptation, allowing signing keys to be used for encryption.

Changed
 - Address Encoding: Switched from hex16 to base32 for the address space.
 - Memory Optimization: Replaced direct memory allocations with streaming for mail processing to reduce RAM overhead.
 - Code Refactoring: Performed decomposition of the original inherited codebase to improve maintainability.

Fixed
 - Mail Updates: Fixed the mail retrieval mechanism to ensure near-instant updates for incoming messages.

Changes to be committed:
	modified:   .gitignore
	modified:   README.md
	modified:   cmd/yggmail/main.go
	modified:   go.mod
	modified:   go.sum
	new file:   internal/account/manager.go
	new file:   internal/account/session.go
	modified:   internal/imapserver/backend.go
	modified:   internal/imapserver/imap.go
	modified:   internal/imapserver/mailbox.go
	modified:   internal/imapserver/notify.go
	modified:   internal/imapserver/user.go
	new file:   internal/notify/notify.go
	new file:   internal/notify/remote/remote.go
	new file:   internal/remote/client.go
	new file:   internal/remote/handlers.go
	new file:   internal/remote/io.go
	new file:   internal/remote/server.go
	new file:   internal/remote/sign.go
	new file:   internal/remote/types/request.pb.go
	new file:   internal/remote/types/request.proto
	new file:   internal/remote/types/response.pb.go
	new file:   internal/remote/types/response.proto
	new file:   internal/shell/shell.go
	modified:   internal/smtpsender/sender.go
	modified:   internal/smtpserver/backend.go
	modified:   internal/smtpserver/session_local.go
	modified:   internal/smtpserver/session_remote.go
	modified:   internal/smtpserver/smtp.go
	new file:   internal/storage/filestore/filestore.go
	new file:   internal/storage/local/storage.go
	new file:   internal/storage/remote/mail.go
	new file:   internal/storage/remote/mailbox.go
	new file:   internal/storage/remote/queue.go
	new file:   internal/storage/remote/storage.go
	new file:   internal/storage/remote/watch.go
	modified:   internal/storage/sqlite3/sqlite3.go
	new file:   internal/storage/sqlite3/table_accounts.go
	modified:   internal/storage/sqlite3/table_mailboxes.go
	modified:   internal/storage/sqlite3/table_mails.go
	modified:   internal/storage/sqlite3/table_queue.go
	modified:   internal/storage/storage.go
	modified:   internal/storage/types/types.go
	modified:   internal/utils/address.go
	new file:   internal/utils/age/age.go
	new file:   internal/utils/age/bech32/bech32.go
	new file:   internal/utils/crypt.go
	new file:   internal/utils/e2ee/crypt.go
	new file:   internal/utils/e2ee/e2ee_test.go
	new file:   internal/utils/e2ee/msg.go
	new file:   internal/utils/x25519.go
	modified:   internal/welcome/welcome.go
2026-02-14 02:34:09 +05:00

228 lines
6.2 KiB
Go

/*
* Original Copyright (c) 2026 Kaiy Ragur
* Original 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/.
*/
package main
import (
"crypto/ed25519"
"flag"
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/fatih/color"
"net/http"
_ "net/http/pprof"
"github.com/neilalexander/yggmail/internal/account"
"github.com/neilalexander/yggmail/internal/config"
"github.com/neilalexander/yggmail/internal/imapserver"
"github.com/neilalexander/yggmail/internal/notify"
"github.com/neilalexander/yggmail/internal/remote"
"github.com/neilalexander/yggmail/internal/shell"
"github.com/neilalexander/yggmail/internal/smtpsender"
"github.com/neilalexander/yggmail/internal/smtpserver"
"github.com/neilalexander/yggmail/internal/storage"
"github.com/neilalexander/yggmail/internal/storage/filestore"
"github.com/neilalexander/yggmail/internal/storage/local"
"github.com/neilalexander/yggmail/internal/storage/sqlite3"
"github.com/neilalexander/yggmail/internal/transport"
"github.com/neilalexander/yggmail/internal/utils"
"github.com/neilalexander/yggmail/internal/welcome"
)
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
}
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
rawlog := log.New(color.Output, "", 0)
green := color.New(color.FgGreen).SprintfFunc()
log := log.New(rawlog.Writer(), fmt.Sprintf("[ %s ] ", green("Yggmail")), log.LstdFlags|log.Lmsgprefix)
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")
mcastregexp := flag.String("mcastregexp", ".*", "Regexp for multicast")
shellMode := flag.String("sh", "", "-sh \"command args\"")
flag.Var(&peerAddrs, "peer", "Connect to a specific Yggdrasil static peer")
flag.Parse()
db, err := sqlite3.NewSQLite3StorageStorage(*database)
if err != nil {
log.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
fs, err := filestore.NewFileStore("./mails/")
if err != nil {
log.Fatalf("Failed to open filesystem: %v", err)
}
storage := local.NewLocalStorage(db, fs, 0)
accManager := account.NewManager(storage, log)
nodeSk, nodePk := getID(storage, log)
cfg := &config.Config{
PublicKey: nodePk,
PrivateKey: nodeSk,
}
if *shellMode != "" {
s := shell.NewShell(storage, log, cfg)
args := strings.Split(*shellMode, " ")
s.Run(args[0], args[1:])
return
}
checkFirstRun(accManager, storage, log, nodePk)
tr, err := transport.NewYggdrasilTransport(rawlog, nodeSk, nodePk, peerAddrs, *multicast, *mcastregexp)
if err != nil {
log.Fatalf("Failed to setup Yggdrasil: %v", err)
}
// Notify bus
notify := notify.NewNotifyBus(log)
// Imap Frontend
imapBackend := imapserver.NewBackend(
cfg,
log,
storage,
accManager,
tr,
notify,
)
srv, err := imapserver.NewIMAPServer(imapBackend, *imapaddr, true)
if err != nil {
log.Fatal(err)
}
imapBackend.Server = srv
// Queues schedule
// For connect with Peers and Push Mail we sign request
nodeSigner := func(data []byte) ([]byte, error) {
return ed25519.Sign(nodeSk, data), nil
}
nodeRPCClient := remote.NewClient(
nodePk,
nodeSigner,
tr,
)
queues := smtpsender.NewQueues(cfg, log, tr, storage, nodeRPCClient, notify)
// SMTP-Frontend
go func() {
localBackend := &smtpserver.Backend{
Log: log,
Mode: smtpserver.BackendModeInternal,
// Config: cfg,
Storage: storage,
Queues: queues,
Notify: notify,
Account: accManager,
NodePublicKey: cfg.PublicKey,
}
server := smtp.NewServer(localBackend)
server.Addr = *smtpaddr
server.Domain = utils.EncodeString(nodePk)
server.AllowInsecureAuth = true
server.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 local SMTP on:", server.Addr)
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}()
// RPC Mail-Server
remoteSrv := remote.NewRemoteServer(cfg.PublicKey, storage, accManager, log, notify, queues)
go func() {
if err := remoteSrv.Serve(tr.Listener()); err != nil {
log.Fatalf("Remote Storage Server failed: %v", err)
}
}()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
log.Println("Shutting down...")
}
func getID(s storage.Storage, l *log.Logger) (ed25519.PrivateKey, ed25519.PublicKey) {
skStr, _ := s.ConfigGet("private_key")
if skStr == "" {
_, sk, _ := ed25519.GenerateKey(nil)
s.ConfigSet("private_key", utils.EncodeString(sk))
l.Println("Generated new Yggdrasil Node Identity")
return sk, sk.Public().(ed25519.PublicKey)
}
skBytes, _ := utils.DecodeString(skStr)
sk := ed25519.PrivateKey(skBytes)
return sk, sk.Public().(ed25519.PublicKey)
}
func checkFirstRun(m *account.Manager, s storage.Storage, l *log.Logger, nodePk ed25519.PublicKey) {
accounts, _ := m.AccountList()
if len(accounts) > 0 {
return
}
l.Println("--- FIRST RUN: CREATE YOUR ACCOUNT ---")
pProvider := func() (string, error) {
p, err := account.DefaultProvider("Enter password for your new mail account: ")
if err != nil {
return "", err
}
p2, err := account.DefaultProvider("Comfirm your password: ")
if err != nil {
return "", err
}
if p == p2 {
return p2, nil
}
return "", fmt.Errorf("Password not match")
}
acc, err := m.CreateNewAccount(pProvider)
if err != nil {
l.Fatalf("Failed to create initial account: %v", err)
}
welcome.Onboard(acc.PublicKey, s, l)
l.Printf("SUCCESS! Your address is: %s@%s\n", acc.PublicKey, utils.EncodeString(nodePk))
l.Println("--------------------------------------")
}