diff --git a/cmd/orchid/conf.go b/cmd/orchid/conf.go index 2377d1d..f84f887 100644 --- a/cmd/orchid/conf.go +++ b/cmd/orchid/conf.go @@ -1,6 +1,9 @@ package main -import "github.com/1f349/orchid/renewal" +import ( + "github.com/1f349/orchid/renewal" + "github.com/1f349/simplemail" +) type startUpConfig struct { Listen string `yaml:"listen"` @@ -8,6 +11,7 @@ type startUpConfig struct { LE renewal.LetsEncryptConfig `yaml:"letsEncrypt"` Domains []string `yaml:"domains"` AgentKey string `yaml:"agentKey"` + Mail mailConfig `yaml:"mail"` } type acmeConfig struct { @@ -15,3 +19,8 @@ type acmeConfig struct { CleanUpUrl string `yaml:"cleanUpUrl"` RefreshUrl string `yaml:"refreshUrl"` } + +type mailConfig struct { + simplemail.Mail `yaml:",inline"` + To simplemail.FromAddress `yaml:"to"` +} diff --git a/cmd/orchid/mail-templates/failed-to-find.go.html b/cmd/orchid/mail-templates/failed-to-find.go.html new file mode 100644 index 0000000..312406a --- /dev/null +++ b/cmd/orchid/mail-templates/failed-to-find.go.html @@ -0,0 +1,10 @@ + + + + Failed to find + + +

Hello

+

World

+ + diff --git a/cmd/orchid/mail-templates/failed-to-find.go.txt b/cmd/orchid/mail-templates/failed-to-find.go.txt new file mode 100644 index 0000000..ab79fac --- /dev/null +++ b/cmd/orchid/mail-templates/failed-to-find.go.txt @@ -0,0 +1 @@ +Failed to find diff --git a/cmd/orchid/mail-templates/failed-to-renew.go.html b/cmd/orchid/mail-templates/failed-to-renew.go.html new file mode 100644 index 0000000..e69de29 diff --git a/cmd/orchid/mail-templates/failed-to-renew.go.txt b/cmd/orchid/mail-templates/failed-to-renew.go.txt new file mode 100644 index 0000000..e69de29 diff --git a/cmd/orchid/main.go b/cmd/orchid/main.go index fabd35c..0225924 100644 --- a/cmd/orchid/main.go +++ b/cmd/orchid/main.go @@ -1,6 +1,8 @@ package main import ( + "embed" + "errors" "flag" "github.com/1f349/mjwt" "github.com/1f349/orchid" @@ -9,10 +11,13 @@ import ( "github.com/1f349/orchid/logger" "github.com/1f349/orchid/renewal" "github.com/1f349/orchid/servers" + "github.com/1f349/overlapfs" + "github.com/1f349/simplemail" "github.com/1f349/violet/utils" _ "github.com/mattn/go-sqlite3" exitReload "github.com/mrmelon54/exit-reload" "gopkg.in/yaml.v3" + "io/fs" "os" "path/filepath" "sync" @@ -20,6 +25,9 @@ import ( var configPath string +//go:embed mail-templates/* +var mailTemplates embed.FS + func main() { flag.StringVar(&configPath, "conf", "", "/path/to/config.json : path to the config file") flag.Parse() @@ -70,6 +78,24 @@ func runDaemon(wd string, conf startUpConfig) { logger.Logger.Fatal("Failed to load MJWT verifier public key from file", "path", filepath.Join(wd, "keys"), "err", err) } + // get mail templates + mailDir := filepath.Join(wd, "mail-templates") + err = os.Mkdir(mailDir, os.ModePerm) + if err != nil && !errors.Is(err, os.ErrExist) { + return + } + wdFs := os.DirFS(mailDir) + mailTemplatesSub, err := fs.Sub(mailTemplates, "mail-templates") + if err != nil { + logger.Logger.Fatal("Failed to load embedded mail templates", "err", err) + } + templatesFS := overlapfs.OverlapFS{A: mailTemplatesSub, B: wdFs} + + mail, err := simplemail.New(&conf.Mail.Mail, templatesFS) + if err != nil { + logger.Logger.Fatal("Failed to load email sender", "err", err) + } + // open sqlite database db, err := orchid.InitDB(filepath.Join(wd, "orchid.db.sqlite")) if err != nil { @@ -84,7 +110,7 @@ func runDaemon(wd string, conf startUpConfig) { if err != nil { logger.Logger.Fatal("HTTP Acme Error", "err", err) } - renewalService, err := renewal.NewService(wg, db, acmeProv, conf.LE, certDir, keyDir) + renewalService, err := renewal.NewService(wg, db, acmeProv, conf.LE, certDir, keyDir, mail, conf.Mail.To) if err != nil { logger.Logger.Fatal("Service Error", "err", err) } diff --git a/go.mod b/go.mod index 5864289..f0be7a4 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.23.5 require ( github.com/1f349/mjwt v0.4.1 + github.com/1f349/overlapfs v0.0.1 + github.com/1f349/simplemail v0.0.8 github.com/1f349/violet v0.0.14 github.com/AlecAivazis/survey/v2 v2.3.7 github.com/bramvdbogaerde/go-scp v1.5.0 @@ -30,6 +32,9 @@ require ( github.com/charmbracelet/lipgloss v1.0.0 // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emersion/go-message v0.18.2 // indirect + github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect + github.com/emersion/go-smtp v0.21.3 // indirect github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/google/go-querystring v1.1.0 // indirect diff --git a/go.sum b/go.sum index 127360b..fc3beae 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,11 @@ github.com/1f349/mjwt v0.4.1 h1:ooCroMMw2kcL5c9L3sLbdtxI0H4/QC8RfTxiloKr+4Y= github.com/1f349/mjwt v0.4.1/go.mod h1:qwnzokkqc7Z9YmKA1m9beI3OZL1GvGYHOQU2rOwoV1M= +github.com/1f349/overlapfs v0.0.1 h1:LAxBolrXFAgU0yqZtXg/C/aaPq3eoQSPpBc49BHuTp0= +github.com/1f349/overlapfs v0.0.1/go.mod h1:I6aItQycr7nrzplmfNXp/QF9tTmKRSgY3fXmu/7Ky2o= github.com/1f349/rsa-helper v0.0.2 h1:N/fLQqg5wrjIzG6G4zdwa5Xcv9/jIPutCls9YekZr9U= github.com/1f349/rsa-helper v0.0.2/go.mod h1:VUQ++1tYYhYrXeOmVFkQ82BegR24HQEJHl5lHbjg7yg= +github.com/1f349/simplemail v0.0.8 h1:stcNaTwt/21K9fMtpDS4Y5wMKkTGHKk1CaU7M3fcSA4= +github.com/1f349/simplemail v0.0.8/go.mod h1:ppAIqkvVkI6L99EefbR5NgOjpePNK/RKgeoehj5A+kU= github.com/1f349/violet v0.0.14 h1:MpBZ4n1dJjdiIwYMTfh0PBIFll3kjqowxR6DLasafqE= github.com/1f349/violet v0.0.14/go.mod h1:iAREhm+wxnGXkmuvmBhOuhUx2T7/5w7stLYNgQGbqC8= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= @@ -28,6 +32,13 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg= +github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk= +github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-smtp v0.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGVMY= +github.com/emersion/go-smtp v0.21.3/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/go-acme/lego/v4 v4.22.2 h1:ck+HllWrV/rZGeYohsKQ5iKNnU/WAZxwOdiu6cxky+0= github.com/go-acme/lego/v4 v4.22.2/go.mod h1:E2FndyI3Ekv0usNJt46mFb9LVpV/XBYT+4E3tz02Tzo= github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= @@ -115,15 +126,18 @@ golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5 golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -132,22 +146,27 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/renewal/service.go b/renewal/service.go index a0e2655..d535569 100644 --- a/renewal/service.go +++ b/renewal/service.go @@ -14,6 +14,7 @@ import ( "github.com/1f349/orchid/database" "github.com/1f349/orchid/pebble" "github.com/1f349/orchid/utils" + "github.com/1f349/simplemail" "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" @@ -65,10 +66,12 @@ type Service struct { keyDir string insecure bool client *lego.Client + mail *simplemail.SimpleMail + mailTo simplemail.FromAddress } // NewService creates a new certificate renewal service. -func NewService(wg *sync.WaitGroup, db *database.Queries, httpAcme challenge.Provider, leConfig LetsEncryptConfig, certDir, keyDir string) (*Service, error) { +func NewService(wg *sync.WaitGroup, db *database.Queries, httpAcme challenge.Provider, leConfig LetsEncryptConfig, certDir, keyDir string, mail *simplemail.SimpleMail, mailTo simplemail.FromAddress) (*Service, error) { s := &Service{ db: db, httpAcme: httpAcme, @@ -81,6 +84,8 @@ func NewService(wg *sync.WaitGroup, db *database.Queries, httpAcme challenge.Pro certDir: certDir, keyDir: keyDir, insecure: leConfig.insecure, + mail: mail, + mailTo: mailTo, } // make certDir and keyDir @@ -125,6 +130,26 @@ func (s *Service) Shutdown() { close(s.certDone) } +func (s *Service) sendErrorEmail(templateName, subject string, data map[string]any, err error) error { + // sending mail is disabled + if s.mail == nil { + return nil + } + + // create empty data map + if data == nil { + data = make(map[string]any) + } + data["error"] = err + + // send email and listen for errors + mailErr := s.mail.Send(templateName, subject, s.mailTo.ToMailAddress(), data) + if mailErr != nil { + return fmt.Errorf("failed to send error email for: %w because: %w", err, mailErr) + } + return err +} + // resolveLEPrivKey resolves the private key for the LetsEncrypt account. // If the string is a path to a file then the contents of the file is read. func (s *Service) resolveLEPrivKey(a string) error { @@ -255,7 +280,7 @@ func (s *Service) renewalCheck() error { // check for running out certificates in the database localData, err := s.findNextCertificateToRenew() if err != nil { - return fmt.Errorf("failed to find a certificate to renew: %w", err) + return s.sendErrorEmail("failed-to-find", "Failed to find a certificate to renew", nil, fmt.Errorf("failed to find a certificate to renew: %w", err)) } // no certificates to update @@ -268,7 +293,12 @@ func (s *Service) renewalCheck() error { err = s.renewCert(localData) if err != nil { Logger.Debug("Failed to renew certificate", "err", err) - return err + return s.sendErrorEmail("failed-to-renew", "Failed to renew a certificate", map[string]any{ + "id": localData.id, + "dns-name": localData.dns.name, + "domains": localData.domains, + "not-after": localData.notAfter, + }, fmt.Errorf("failed to renew a certificate: %w", err)) } // renew succeeded diff --git a/renewal/service_test.go b/renewal/service_test.go index 2d71c70..feb79a6 100644 --- a/renewal/service_test.go +++ b/renewal/service_test.go @@ -13,6 +13,7 @@ import ( "github.com/1f349/orchid/logger" "github.com/1f349/orchid/pebble" "github.com/1f349/orchid/test" + "github.com/1f349/simplemail" "github.com/charmbracelet/log" "github.com/go-acme/lego/v4/lego" "github.com/google/uuid" @@ -125,7 +126,7 @@ func setupPebbleTest(t *testing.T, serverTls *certgen.CertGen) (*Service, *sql.D Directory: "https://localhost:14000/dir", Certificate: "insecure", insecure: true, - }, certDir, keyDir) + }, certDir, keyDir, nil, simplemail.FromAddress{}) fmt.Println(err) assert.NoError(t, err)