ygggo/internal/shell/shell.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

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
}