- 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
204 lines
4.6 KiB
Go
204 lines
4.6 KiB
Go
/*
|
|
* Copyright (c) 2026 Kaiy Ragur
|
|
*
|
|
* 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 shell
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
|
|
"github.com/neilalexander/yggmail/internal/account"
|
|
"github.com/neilalexander/yggmail/internal/config"
|
|
"github.com/neilalexander/yggmail/internal/storage"
|
|
"github.com/neilalexander/yggmail/internal/storage/types"
|
|
"github.com/neilalexander/yggmail/internal/utils"
|
|
)
|
|
|
|
type Shell struct {
|
|
storage storage.Storage
|
|
account *account.Manager
|
|
cfg *config.Config
|
|
}
|
|
|
|
func NewShell(storage storage.Storage, log *log.Logger, cfg *config.Config) *Shell {
|
|
return &Shell{
|
|
storage: storage,
|
|
account: account.NewManager(storage, log),
|
|
cfg: cfg,
|
|
}
|
|
}
|
|
|
|
func passwordProvider() (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")
|
|
}
|
|
|
|
type shellCommand func(args []string) error
|
|
|
|
func (s *Shell) Run(c string, args []string) {
|
|
// fmt.Println(c, args)
|
|
var err error
|
|
|
|
commands := map[string]shellCommand{
|
|
"accountAdd": s.accountAdd,
|
|
"accountCreateNew": s.accountCreateNew,
|
|
"accountGet": s.accountGet,
|
|
"accountList": s.accountList,
|
|
"accountDelete": s.accountDelete,
|
|
"nodeInfo": s.getNodeInfo,
|
|
}
|
|
cmd, ok := commands[c]
|
|
if !ok {
|
|
log.Fatal("Command not found!")
|
|
os.Exit(1)
|
|
}
|
|
err = cmd(args)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
}
|
|
|
|
func (s *Shell) accountAdd(args []string) error {
|
|
|
|
flags := flag.NewFlagSet("addAccount", flag.ExitOnError)
|
|
// All flags
|
|
pubkey := flags.String("pub", "", "Account public key")
|
|
file := flags.String("f", "", "Added from file")
|
|
|
|
flags.Parse(args)
|
|
|
|
var (
|
|
err error
|
|
acc *types.Account = new(types.Account)
|
|
)
|
|
if *file != "" {
|
|
cont, err := os.ReadFile(*file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := json.Unmarshal(cont, acc); err != nil {
|
|
return err
|
|
}
|
|
if err := s.storage.AccountCreate(acc); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Account %s@%s added", acc.PublicKey, utils.EncodeString(s.cfg.PublicKey))
|
|
for _, folder := range []string{"INBOX", "Outbox", "Sent", "Trash", "Drafts"} {
|
|
s.storage.MailboxCreate(acc.PublicKey, folder)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if acc, err = s.account.AddAccount(*pubkey); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Account %s@%s added", acc.PublicKey, utils.EncodeString(s.cfg.PublicKey))
|
|
return nil
|
|
}
|
|
|
|
func (s *Shell) getNodeInfo(args []string) error {
|
|
|
|
flags := flag.NewFlagSet("nodeInfo", flag.ExitOnError)
|
|
flags.Parse(args)
|
|
|
|
pks := utils.EncodeString(s.cfg.PublicKey)
|
|
accList, err := s.storage.AccountList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println("== Node Info == ")
|
|
fmt.Println("Address base32:", pks)
|
|
fmt.Println("Address hex16:", hex.EncodeToString(s.cfg.PublicKey))
|
|
fmt.Println("Accounts: ", len(accList))
|
|
return nil
|
|
}
|
|
|
|
func (s *Shell) accountList(args []string) error {
|
|
accList, err := s.storage.AccountList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println("Accounts: ", len(accList))
|
|
for i, a := range accList {
|
|
count, err := s.storage.MailCount(a.PublicKey, "INBOX")
|
|
if err != nil {
|
|
count = 0
|
|
}
|
|
fmt.Printf("\n| %d | %s | %s | Mails: %d", i, a.PublicKey, a.Aliases, count)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Shell) accountCreateNew(args []string) error {
|
|
a, err := s.account.CreateNewAccount(passwordProvider)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Created account: %s@%s", a.PublicKey, utils.EncodeString(s.cfg.PublicKey))
|
|
return nil
|
|
}
|
|
|
|
func (s *Shell) accountGet(args []string) error {
|
|
flags := flag.NewFlagSet("getAccount", flag.ExitOnError)
|
|
// All flags
|
|
pubkey := flags.String("pub", "", "Account Publickey")
|
|
file := flags.String("o", "", "Write to file")
|
|
|
|
flags.Parse(args)
|
|
|
|
var (
|
|
err error
|
|
acc *types.Account
|
|
)
|
|
|
|
if acc, err = s.account.GetAccount(*pubkey); err != nil {
|
|
return err
|
|
}
|
|
b, err := json.Marshal(acc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if *file == "" {
|
|
fmt.Println(string(b))
|
|
} else {
|
|
os.WriteFile(*file, b, 0600)
|
|
fmt.Println("Write to ", *file)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Shell) accountDelete(args []string) error {
|
|
flags := flag.NewFlagSet("getAccount", flag.ExitOnError)
|
|
// All flags
|
|
pubkey := flags.String("pub", "", "Account Publickey")
|
|
flags.Parse(args)
|
|
|
|
var (
|
|
err error
|
|
)
|
|
if err = s.account.DeleteAccount(*pubkey); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
|
|
}
|