1
0
Fork 0
mirror of https://github.com/Luzifer/automail.git synced 2024-12-20 04:51:20 +00:00
automail/main.go
Knut Ahlers c2687bc39f
Add additional storage provider redis
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-02-25 15:00:48 +01:00

201 lines
5.3 KiB
Go

package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
"github.com/jhillyerd/enmime"
log "github.com/sirupsen/logrus"
"github.com/Luzifer/rconfig/v2"
)
var (
cfg = struct {
Config string `flag:"config,c" default:"config.yaml" description:"Configuration file with instruction"`
FetchInterval time.Duration `flag:"interval,i" default:"1m" description:"Interval to fetch mails"`
IMAPHost string `flag:"imap-host,h" default:"" description:"Host of the IMAP server" validate:"nonzero"`
IMAPPort int `flag:"imap-port" default:"993" description:"Port of the IMAP server" validate:"nonzero"`
IMAPUser string `flag:"imap-user,u" default:"" description:"Username to access the IMAP server" validate:"nonzero"`
IMAPPass string `flag:"imap-pass,p" default:"" description:"Password to access the IMAP server" validate:"nonzero"`
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
Mailbox string `flag:"mailbox,m" default:"INBOX" description:"Mailbox to fetch from"`
StorageType string `flag:"storage-type" default:"file" description:"Driver to use for storing persistent info"`
StorageDSN string `flag:"storage-dsn" default:"store.yaml" description:"Where to store persistent info"`
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
}{}
version = "dev"
)
func init() {
rconfig.AutoEnv(true)
if err := rconfig.ParseAndValidate(&cfg); err != nil {
log.WithError(err).Fatalf("parsing commandline options")
}
if cfg.VersionAndExit {
fmt.Printf("automail %s\n", version)
os.Exit(0)
}
if l, err := log.ParseLevel(cfg.LogLevel); err != nil {
log.WithError(err).Fatal("parsing log level")
} else {
log.SetLevel(l)
}
}
func main() {
bodySection, err := imap.ParseBodySectionName("BODY[]")
if err != nil {
log.WithError(err).Fatal("parsing body section")
}
conf, err := loadConfig()
if err != nil {
log.WithError(err).Fatal("loading config")
}
store, err := newStorage(cfg.StorageType, cfg.StorageDSN)
if err != nil {
log.WithError(err).Fatal("creating storage interface")
}
if err = store.Load(); err != nil {
log.WithError(err).Fatal("loading persistent storage data")
}
var (
imapClient *client.Client
messages = make(chan *imap.Message, 1000)
needLogin = make(chan struct{}, 1)
sigs = make(chan os.Signal, 1)
ticker = time.NewTicker(cfg.FetchInterval)
)
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
needLogin <- struct{}{}
for {
select {
case <-needLogin:
if imapClient != nil {
imapClient.Close()
}
imapClient, err = client.DialTLS(fmt.Sprintf("%s:%d", cfg.IMAPHost, cfg.IMAPPort), nil)
if err != nil {
log.WithError(err).Fatal("connecting to IMAP server")
}
if err = imapClient.Login(cfg.IMAPUser, cfg.IMAPPass); err != nil {
log.WithError(err).Fatal("loggin in to IMAP server")
}
log.Info("IMAP connected and logged in")
if _, err = imapClient.Select(cfg.Mailbox, false); err != nil {
log.WithError(err).Fatal("selecting mailbox")
}
go func() {
// Trigger re-login when log-out was received
<-imapClient.LoggedOut()
needLogin <- struct{}{}
}()
case <-ticker.C:
if _, err := imapClient.Select(cfg.Mailbox, false); err != nil {
log.WithError(err).Error("selecting mailbox")
continue
}
seq, err := imap.ParseSeqSet(fmt.Sprintf("%d:*", store.GetLastUID()+1))
if err != nil {
log.WithError(err).Error("parsing sequence set")
continue
}
ids, err := imapClient.UidSearch(&imap.SearchCriteria{
Uid: seq,
})
if err != nil {
log.WithError(err).Error("searching for messages")
continue
}
if len(ids) == 0 {
continue
}
tmpMsg := make(chan *imap.Message)
go func() {
for msg := range tmpMsg {
if msg.Uid <= store.GetLastUID() {
continue
}
messages <- msg
}
}()
fetchSeq := &imap.SeqSet{}
fetchSeq.AddNum(ids...)
if err = imapClient.UidFetch(fetchSeq, []imap.FetchItem{
imap.FetchFlags,
imap.FetchItem("BODY.PEEK[]"),
imap.FetchUid,
}, tmpMsg); err != nil {
log.WithError(err).Error("fetching messages")
continue
}
case <-sigs:
return
case msg := <-messages:
body := msg.GetBody(bodySection)
if body == nil {
log.WithField("uid", msg.Uid).Debug("Got message with nil body")
continue
}
mail, err := enmime.ReadEnvelope(body)
if err != nil {
log.WithError(err).Error("parsing message")
continue
}
log.WithFields(log.Fields{
"subject": mail.GetHeader("subject"),
"uid": msg.Uid,
}).Debug("Fetched message")
// Check all handlers whether they want to handle the message
for _, hdl := range conf.Handlers {
if hdl.Handles(mail) {
go func(msg *imap.Message, hdl mailHandler) {
if err := hdl.Process(imapClient, msg, mail); err != nil {
log.WithError(err).Error("processing message")
}
}(msg, hdl)
}
}
// Mark message as processed in store
if msg.Uid > store.GetLastUID() {
store.SetUID(msg.Uid)
if err = store.Save(); err != nil {
log.WithError(err).Error("saving storage")
}
}
}
}
}