2024-12-10 19:54:22 +00:00
package main
import (
2025-01-17 18:13:01 +00:00
library "git.ailur.dev/ailur/fg-library/v3"
nucleusLibrary "git.ailur.dev/ailur/fg-nucleus-library"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/imapserver"
"github.com/golang-jwt/jwt/v5"
2024-12-10 19:54:22 +00:00
"bytes"
"errors"
2024-12-12 21:08:05 +00:00
"fmt"
2024-12-10 19:54:22 +00:00
"io"
"net"
"os"
2024-12-12 21:08:05 +00:00
"strings"
2024-12-10 19:54:22 +00:00
"time"
"crypto/ed25519"
"crypto/tls"
2025-01-17 18:13:01 +00:00
"encoding/base64"
2024-12-10 19:54:22 +00:00
"encoding/json"
"net/http"
"net/textproto"
2025-01-17 18:13:01 +00:00
"git.ailur.dev/ailur/smtp"
"github.com/google/uuid"
2024-12-10 19:54:22 +00:00
)
var ServiceInformation = library . Service {
Name : "kittemail" ,
Permissions : library . Permissions {
Authenticate : true ,
2025-01-17 18:13:01 +00:00
Router : false ,
2024-12-10 19:54:22 +00:00
Database : true ,
BlobStorage : true ,
InterServiceCommunication : true ,
Resources : false ,
} ,
ServiceID : uuid . MustParse ( "068b0c04-d8c8-4738-90fa-d3827f5abf68" ) ,
}
2025-01-17 18:13:01 +00:00
var (
loggerService = uuid . MustParse ( "00000000-0000-0000-0000-000000000002" )
storageService = uuid . MustParse ( "00000000-0000-0000-0000-000000000003" )
)
2024-12-10 19:54:22 +00:00
// Log a message to the logger service
// messageType:
// 0 - Information
// 1 - Warning
// 2 - Error
// 3 - Guru (exits immediately)
2025-01-17 18:13:01 +00:00
func log ( message string , messageType library . MessageCode ) {
// Log the message to the logger service
Information . SendISMessage ( loggerService , messageType , message )
2024-12-10 19:54:22 +00:00
}
2024-12-12 21:08:05 +00:00
// Authenticate Fake for testing
2025-01-17 18:13:01 +00:00
func Authenticate ( _ string , _ OAuthConfig ) ( uuid . UUID , error ) {
2024-12-12 21:08:05 +00:00
println ( "called" )
2025-01-17 18:13:01 +00:00
return uuid . MustParse ( "ef585ba0-0ac6-48f8-8ebb-2b6cd97d6a21" ) , nil
2024-12-12 21:08:05 +00:00
}
// GetUsername Fake for testing
2025-01-17 18:13:01 +00:00
func GetUsername ( _ string , _ OAuthConfig ) ( string , error ) {
2024-12-12 21:08:05 +00:00
return "arzumify" , nil
}
func RAuthenticate ( token string , config OAuthConfig ) ( uuid . UUID , error ) {
2024-12-10 19:54:22 +00:00
parsedToken , err := jwt . Parse ( token , func ( token * jwt . Token ) ( interface { } , error ) {
return config . PublicKey , nil
} )
if err != nil {
return uuid . Nil , err
}
if ! parsedToken . Valid {
return uuid . Nil , errors . New ( "invalid token" )
}
claims , ok := parsedToken . Claims . ( jwt . MapClaims )
if ! ok {
return uuid . Nil , errors . New ( "invalid token" )
}
date , err := claims . GetExpirationTime ( )
if err != nil || date . Before ( time . Now ( ) ) || claims [ "sub" ] == nil || claims [ "isOpenID" ] == nil || claims [ "isOpenID" ] . ( bool ) {
return uuid . Nil , errors . New ( "invalid token" )
}
return uuid . MustParse ( claims [ "sub" ] . ( string ) ) , nil
}
2024-12-12 21:08:05 +00:00
func RGetUsername ( token string , config OAuthConfig ) ( string , error ) {
2024-12-10 19:54:22 +00:00
var responseData struct {
Username string ` json:"username" `
}
request , err := http . NewRequest ( "GET" , config . HostName + "/api/oauth/userinfo" , nil )
if err != nil {
return "" , err
}
request . Header . Set ( "Authorization" , "Bearer " + token )
response , err := http . DefaultClient . Do ( request )
if err != nil {
return "" , err
}
if response . StatusCode != 200 || response . Body == nil || response . Body == http . NoBody {
return "" , errors . New ( "invalid response" )
}
err = json . NewDecoder ( response . Body ) . Decode ( & responseData )
if err != nil {
return "" , err
}
return responseData . Username , nil
}
2025-01-17 18:13:01 +00:00
func StoreFile ( name string , data * io . LimitedReader , owner uuid . UUID ) error {
_ , err := Information . SendAndAwaitISMessage ( storageService , 3 , nucleusLibrary . File {
Name : name ,
User : owner ,
Reader : data ,
2024-12-10 19:54:22 +00:00
} , 3 * time . Second )
if err != nil {
return err
}
2025-01-17 18:13:01 +00:00
return nil
2024-12-10 19:54:22 +00:00
}
func GetFile ( name string , owner uuid . UUID ) ( * os . File , error ) {
2025-01-17 18:13:01 +00:00
response , err := Information . SendAndAwaitISMessage ( storageService , 4 , nucleusLibrary . File {
Name : name ,
User : owner ,
2024-12-10 19:54:22 +00:00
} , 3 * time . Second )
if err != nil {
return nil , err
}
2025-01-17 18:13:01 +00:00
file , ok := response . Message . ( * os . File )
if ! ok {
return nil , errors . New ( "invalid response" )
2024-12-10 19:54:22 +00:00
}
2025-01-17 18:13:01 +00:00
return file , nil
2024-12-10 19:54:22 +00:00
}
func DeleteFile ( name string , owner uuid . UUID ) error {
2025-01-17 18:13:01 +00:00
_ , err := Information . SendAndAwaitISMessage ( storageService , 5 , nucleusLibrary . File {
Name : name ,
User : owner ,
2024-12-10 19:54:22 +00:00
} , 3 * time . Second )
if err != nil {
return err
}
2025-01-17 18:13:01 +00:00
return nil
2024-12-10 19:54:22 +00:00
}
func beginInitialisation ( hostName string ) ( database library . Database , publicKey ed25519 . PublicKey , oauthHostName string , oauthRegistration nucleusLibrary . OAuthResponse , err error ) {
2025-01-17 18:13:01 +00:00
database , err = Information . GetDatabase ( )
2024-12-10 19:54:22 +00:00
if err != nil {
return
}
2025-01-17 18:13:01 +00:00
// Initialize the database
err = setupDatabase ( database )
2024-12-10 19:54:22 +00:00
if err != nil {
return
}
2025-01-17 18:13:01 +00:00
// Initialize the OAuth
oauthRegistration , publicKey , oauthHostName , err = nucleusLibrary . InitializeOAuth ( nucleusLibrary . OAuthInformation {
Name : "kittemail" ,
RedirectUri : hostName + "/oauth" ,
Scopes : [ ] string { "openid" } ,
} , Information )
2024-12-10 19:54:22 +00:00
if err != nil {
return
}
return
}
func setupDatabase ( database library . Database ) error {
if database . DBType == library . Sqlite {
_ , err := database . DB . Exec ( "CREATE TABLE IF NOT EXISTS emails (id BLOB NOT NULL PRIMARY KEY, prefix TEXT NOT NULL, suffix TEXT NOT NULL, creator BLOB NOT NULL, UNIQUE(prefix, suffix))" )
if err != nil {
return err
}
_ , err = database . DB . Exec ( "CREATE TABLE IF NOT EXISTS mailboxes (id BLOB NOT NULL PRIMARY KEY, owner BLOB NOT NULL, mailbox TEXT NOT NULL, subscribed BOOLEAN NOT NULL DEFAULT FALSE, UNIQUE(mailbox, owner))" )
if err != nil {
return err
}
2025-01-17 18:13:01 +00:00
_ , err = database . DB . Exec ( "CREATE TABLE IF NOT EXISTS messages (id BLOB NOT NULL PRIMARY KEY, owner BLOB NOT NULL, uid INTEGER NOT NULL, created INTEGER NOT NULL, bodySize INTEGER NOT NULL, mailbox BLOB NOT NULL, FOREIGN KEY (mailbox) REFERENCES mailboxes(id))" )
if err != nil {
return err
}
_ , err = database . DB . Exec ( "CREATE TABLE IF NOT EXISTS flags (id BLOB NOT NULL, flag TEXT NOT NULL, FOREIGN KEY (id) REFERENCES messages(id), UNIQUE(id, flag))" )
2024-12-10 19:54:22 +00:00
if err != nil {
return err
}
} else {
_ , err := database . DB . Exec ( "CREATE TABLE IF NOT EXISTS emails (id BYTEA NOT NULL PRIMARY KEY, prefix TEXT NOT NULL, suffix TEXT NOT NULL, creator BYTEA NOT NULL, UNIQUE(prefix, suffix))" )
if err != nil {
return err
}
_ , err = database . DB . Exec ( "CREATE TABLE IF NOT EXISTS mailboxes (id BYTEA NOT NULL PRIMARY KEY, owner BLOB NOT NULL, mailbox TEXT NOT NULL, subscribed BOOLEAN NOT NULL DEFAULT FALSE, UNIQUE(mailbox, owner))" )
if err != nil {
return err
}
2025-01-17 18:13:01 +00:00
_ , err = database . DB . Exec ( "CREATE TABLE IF NOT EXISTS messages (id BYTEA NOT NULL PRIMARY KEY, owner BYTEA NOT NULL, uid BIGINT NOT NULL, created INTEGER NOT NULL, bodySize BIGINT NOT NULL, mailbox BYTEA NOT NULL, FOREIGN KEY (mailbox) REFERENCES mailboxes(id))" )
if err != nil {
return err
}
_ , err = database . DB . Exec ( "CREATE TABLE IF NOT EXISTS flags (id BYTEA NOT NULL, flag TEXT NOT NULL, FOREIGN KEY (id) REFERENCES messages(id), UNIQUE(id, flag))" )
2024-12-10 19:54:22 +00:00
if err != nil {
return err
}
}
return nil
}
var (
2025-01-17 18:13:01 +00:00
Information * library . ServiceInitializationInformation
2024-12-10 19:54:22 +00:00
Database library . Database
SMTPDatabaseBackend = smtp . DatabaseBackend {
CheckUser : func ( address * smtp . Address ) ( bool , error ) {
var prefix , suffix string
err := Database . DB . QueryRow ( "SELECT prefix, suffix FROM emails WHERE prefix = $1 AND suffix = $2" , address . Name , address . Address ) . Scan ( & prefix , & suffix )
if err != nil {
return false , errors . New ( "503 5.5.1 User not found" )
}
if prefix == address . Name && suffix == address . Address {
return true , nil
} else {
return false , nil
}
} ,
WriteMail : func ( mail * smtp . Mail ) error {
for _ , recipient := range mail . To {
var idRaw [ ] byte
err := Database . DB . QueryRow ( "SELECT creator FROM emails WHERE prefix = $1 AND suffix = $2" , recipient . Name , recipient . Address ) . Scan ( & idRaw )
if err != nil {
return errors . New ( "503 5.5.1 User not found" )
}
user := & User {
2025-01-17 18:13:01 +00:00
sub : uuid . Must ( uuid . FromBytes ( idRaw ) ) ,
2024-12-10 19:54:22 +00:00
}
2025-01-17 18:13:01 +00:00
mailbox , err := user . mailbox ( "INBOX" )
2024-12-10 19:54:22 +00:00
if err != nil {
return errors . New ( "503 5.5.1 User not found" )
}
2025-01-17 18:13:01 +00:00
view := mailbox . NewView ( )
_ , err = view . appendBuffer ( bytes . NewReader ( mail . Data ) , int64 ( len ( mail . Data ) ) , & imap . AppendOptions {
Time : time . Now ( ) ,
2024-12-10 19:54:22 +00:00
} )
if err != nil {
2025-01-17 18:13:01 +00:00
err = view . Close ( )
if err != nil {
log ( "Failed to close view: " + err . Error ( ) + ", resource leaks may occur" , 1 )
}
2024-12-10 19:54:22 +00:00
return errors . New ( "421 4.7.0 Error storing message" )
}
return nil
}
return nil
} ,
}
2024-12-12 21:08:05 +00:00
)
func NewSMTPAuthenticationBackend ( OAuthRegistration OAuthConfig ) smtp . AuthenticationBackend {
return smtp . AuthenticationBackend {
SupportedMechanisms : [ ] string { "PLAIN" , "XOAUTH2" , "OAUTHBEARER" } ,
Authenticate : func ( initial string , conn * textproto . Conn ) ( checkAddr smtp . CheckAddress , finalErr error ) {
initialResponse := strings . SplitN ( initial , " " , 2 )
var username , token string
switch initialResponse [ 0 ] {
case "PLAIN" :
credentials , err := base64 . StdEncoding . DecodeString ( initialResponse [ 1 ] )
if err != nil {
return nil , errors . New ( "421 4.7.0 Malformed credentials" )
}
credentialSlice := bytes . SplitN ( bytes . TrimPrefix ( credentials , [ ] byte { 0x00 } ) , [ ] byte { 0x00 } , 2 )
username = string ( credentialSlice [ 0 ] )
token = string ( credentialSlice [ 1 ] )
case "OAUTHBEARER" , "XOAUTH2" :
credentials , err := base64 . StdEncoding . DecodeString ( initialResponse [ 1 ] )
if err != nil {
return nil , errors . New ( "421 4.7.0 Malformed credentials" )
}
credentialSlice := bytes . SplitN ( bytes . TrimSuffix ( bytes . TrimPrefix ( credentials , [ ] byte ( "user=" ) ) , [ ] byte { 0x01 , 0x01 } ) , [ ] byte { 0x01 } , 2 )
username = string ( credentialSlice [ 0 ] )
token = string ( credentialSlice [ 1 ] )
default :
return nil , errors . New ( "503 5.5.1 Invalid authentication method: " + initialResponse [ 0 ] )
}
fmt . Println ( "Username: " + username )
fmt . Println ( "Token: " + token )
sub , err := Authenticate ( token , OAuthRegistration )
if err != nil {
return nil , errors . New ( "421 4.7.0 Invalid credentials" )
}
usernameCheck , err := GetUsername ( token , OAuthRegistration )
if err != nil {
return nil , errors . New ( "421 4.7.0 Invalid credentials" )
}
if username != usernameCheck {
return nil , errors . New ( "421 4.7.0 Username does not match" )
}
return func ( address * smtp . Address ) ( bool , error ) {
rows , err := Database . DB . Query ( "SELECT prefix, suffix FROM emails WHERE creator = $1" , sub [ : ] )
2024-12-10 19:54:22 +00:00
if err != nil {
2024-12-12 21:08:05 +00:00
return false , err
2024-12-10 19:54:22 +00:00
}
2024-12-12 21:08:05 +00:00
defer func ( ) {
err := rows . Close ( )
2024-12-10 19:54:22 +00:00
if err != nil {
2024-12-12 21:08:05 +00:00
log ( "Failed to close rows: " + err . Error ( ) + ", resource leaks may occur" , 1 )
2024-12-10 19:54:22 +00:00
}
2024-12-12 21:08:05 +00:00
} ( )
2024-12-10 19:54:22 +00:00
2024-12-12 21:08:05 +00:00
for rows . Next ( ) {
var prefix , suffix string
err = rows . Scan ( & prefix , & suffix )
if err != nil {
return false , err
}
2024-12-10 19:54:22 +00:00
2024-12-12 21:08:05 +00:00
if address . Name == prefix && address . Address == suffix {
return true , nil
2024-12-10 19:54:22 +00:00
}
2024-12-12 21:08:05 +00:00
}
2024-12-10 19:54:22 +00:00
2024-12-12 21:08:05 +00:00
return false , nil
} , nil
} ,
2024-12-10 19:54:22 +00:00
}
2024-12-12 21:08:05 +00:00
}
2024-12-10 19:54:22 +00:00
func parseConfig ( ) ( hostName string , listenerHost string , ownedDomains [ ] string , enforceTLS bool , enableTLS bool , certificatePath string , keyPath string , err error ) {
var ok bool
hostName , ok = Information . Configuration [ "hostName" ] . ( string )
if ! ok {
err = errors . New ( "hostName not found" )
return
}
listenerHost , ok = Information . Configuration [ "listenerHost" ] . ( string )
if ! ok {
err = errors . New ( "listenerHost not found" )
return
}
ownedDomains , ok = Information . Configuration [ "ownedDomains" ] . ( [ ] string )
if ! ok {
err = errors . New ( "ownedDomains not found" )
return
}
enforceTLS , ok = Information . Configuration [ "enforceTLS" ] . ( bool )
if ! ok {
err = errors . New ( "enforceTLS not found" )
return
}
enableTLS , ok = Information . Configuration [ "enableTLS" ] . ( bool )
if ! ok {
err = errors . New ( "enableTLS not found" )
return
}
if enforceTLS && ! enableTLS {
err = errors . New ( "cannot enforce TLS without enabling it" )
return
}
if enableTLS {
tlsConfiguration , ok := Information . Configuration [ "tlsConfiguration" ] . ( map [ string ] interface { } )
if ! ok {
err = errors . New ( "tlsConfiguration not found" )
return
}
certificatePath , ok = tlsConfiguration [ "certificatePath" ] . ( string )
if ! ok {
err = errors . New ( "certificatePath not found" )
return
}
keyPath , ok = tlsConfiguration [ "keyPath" ] . ( string )
if ! ok {
err = errors . New ( "keyPath not found" )
return
}
}
return
}
func getTLSConfig ( enableTLS bool , certificatePath string , keyPath string ) ( tlsConfig * tls . Config , err error ) {
if enableTLS {
certificate , err := tls . LoadX509KeyPair ( certificatePath , keyPath )
if err != nil {
return nil , err
}
tlsConfig = & tls . Config {
Certificates : [ ] tls . Certificate { certificate } ,
}
} else {
tlsConfig = & tls . Config {
GetCertificate : func ( info * tls . ClientHelloInfo ) ( * tls . Certificate , error ) {
return nil , errors . New ( "TLS disabled in configuration" )
} ,
}
}
return
}
2025-01-17 18:13:01 +00:00
func Main ( information * library . ServiceInitializationInformation ) {
2024-12-10 19:54:22 +00:00
Information = information
hostName , listenerHost , ownedDomains , enforceTLS , enableTLS , certificatePath , keyPath , err := parseConfig ( )
2025-01-17 18:13:01 +00:00
go information . StartISProcessor ( )
2024-12-10 19:54:22 +00:00
var oauthConfig OAuthConfig
// var oauthRegistration nucleusLibrary.OAuthResponse
Database , oauthConfig . PublicKey , oauthConfig . HostName , _ , err = beginInitialisation ( hostName )
if err != nil {
log ( "Failed to initialise: " + err . Error ( ) , 3 )
return
}
2025-01-17 18:13:01 +00:00
imapBackend := New ( oauthConfig )
2024-12-10 19:54:22 +00:00
tlsConfig , err := getTLSConfig ( enableTLS , certificatePath , keyPath )
if err != nil {
log ( "Failed to get TLS config: " + err . Error ( ) , 3 )
return
}
// Plaintext IMAP port
go func ( ) {
2025-01-17 18:13:01 +00:00
imapOptions := & imapserver . Options {
NewSession : func ( conn * imapserver . Conn ) ( imapserver . Session , * imapserver . GreetingData , error ) {
return imapBackend . NewSession ( ) , nil , nil
} ,
Caps : imap . CapSet {
imap . CapIMAP4rev1 : { } ,
imap . CapIMAP4rev2 : { } ,
imap . CapBinary : { } ,
} ,
TLSConfig : tlsConfig ,
// TODO: Remove for production
DebugWriter : os . Stdout ,
}
2024-12-10 19:54:22 +00:00
if ! enforceTLS {
2025-01-17 18:13:01 +00:00
imapOptions . InsecureAuth = true
2024-12-10 19:54:22 +00:00
}
2025-01-17 18:13:01 +00:00
listener , err := net . Listen ( "tcp" , listenerHost + ":143" )
if err != nil {
log ( "Failed to listen on port 143: " + err . Error ( ) , 3 )
return
}
err = imapserver . New ( imapOptions ) . Serve ( listener )
2024-12-10 19:54:22 +00:00
if err != nil {
log ( "Failed to serve IMAP: " + err . Error ( ) , 3 )
}
} ( )
// Implicit TLS IMAP port
go func ( ) {
2025-01-17 18:13:01 +00:00
imapOptions := & imapserver . Options {
NewSession : func ( conn * imapserver . Conn ) ( imapserver . Session , * imapserver . GreetingData , error ) {
return imapBackend . NewSession ( ) , nil , nil
} ,
Caps : imap . CapSet {
imap . CapIMAP4rev1 : { } ,
imap . CapIMAP4rev2 : { } ,
} ,
TLSConfig : tlsConfig ,
// TODO: Remove for production
DebugWriter : os . Stdout ,
}
2024-12-10 19:54:22 +00:00
if ! enforceTLS {
2025-01-17 18:13:01 +00:00
imapOptions . InsecureAuth = true
}
listener , err := tls . Listen ( "tcp" , listenerHost + ":993" , tlsConfig )
if err != nil {
log ( "Failed to listen on port 993: " + err . Error ( ) , 3 )
return
2024-12-10 19:54:22 +00:00
}
2025-01-17 18:13:01 +00:00
err = imapserver . New ( imapOptions ) . Serve ( listener )
2024-12-10 19:54:22 +00:00
if err != nil {
log ( "Failed to serve IMAP: " + err . Error ( ) , 3 )
}
} ( )
var smtpTLSConfig * tls . Config
if enableTLS {
smtpTLSConfig = tlsConfig
}
// SMTP Proxy port
go func ( ) {
smtpListener , err := net . Listen ( "tcp" , ":25" )
if err != nil {
log ( "Failed to listen on port 25: " + err . Error ( ) , 3 )
return
}
2024-12-12 21:08:05 +00:00
2024-12-10 19:54:22 +00:00
smtpBackend := smtp . NewReceiver ( smtpListener , hostName , ownedDomains , enforceTLS , SMTPDatabaseBackend , NewSMTPAuthenticationBackend ( oauthConfig ) , smtpTLSConfig )
err = smtpBackend . Serve ( )
if err != nil {
log ( "Failed to serve SMTP: " + err . Error ( ) , 3 )
}
} ( )
// Plaintext submission port
go func ( ) {
smtpListener , err := net . Listen ( "tcp" , ":587" )
if err != nil {
log ( "Failed to listen on port 587: " + err . Error ( ) , 3 )
return
}
smtpBackend := smtp . NewReceiver ( smtpListener , hostName , ownedDomains , enforceTLS , SMTPDatabaseBackend , NewSMTPAuthenticationBackend ( oauthConfig ) , smtpTLSConfig )
err = smtpBackend . Serve ( )
if err != nil {
log ( "Failed to serve SMTP submission: " + err . Error ( ) , 3 )
}
} ( )
// Implicit TLS Submission port
go func ( ) {
smtpListener , err := net . Listen ( "tcp" , ":465" )
if err != nil {
log ( "Failed to listen on port 587: " + err . Error ( ) , 3 )
return
}
smtpBackend := smtp . NewReceiver ( tls . NewListener ( smtpListener , tlsConfig ) , hostName , ownedDomains , enforceTLS , SMTPDatabaseBackend , NewSMTPAuthenticationBackend ( oauthConfig ) , smtpTLSConfig )
err = smtpBackend . Serve ( )
if err != nil {
log ( "Failed to serve SMTP submission: " + err . Error ( ) , 3 )
}
} ( )
}