2020-06-27 13:25:16 +00:00
|
|
|
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"`
|
|
|
|
StorageFile string `flag:"storage-file" 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.Fatalf("Unable to parse commandline options: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
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("Unable to parse log level")
|
|
|
|
} else {
|
|
|
|
log.SetLevel(l)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
bodySection, err := imap.ParseBodySectionName("BODY[]")
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Fatal("Unable to parse body section")
|
|
|
|
}
|
|
|
|
|
|
|
|
conf, err := loadConfig()
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Fatal("Unable to load config")
|
|
|
|
}
|
|
|
|
|
|
|
|
store, err := loadStorage()
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Fatal("Unable to load storage file")
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
2020-06-28 15:51:52 +00:00
|
|
|
imapClient *client.Client
|
|
|
|
messages = make(chan *imap.Message, 1000)
|
|
|
|
needLogin = make(chan struct{}, 1)
|
2020-06-28 15:53:37 +00:00
|
|
|
sigs = make(chan os.Signal, 1)
|
2020-06-28 15:51:52 +00:00
|
|
|
ticker = time.NewTicker(cfg.FetchInterval)
|
2020-06-27 13:25:16 +00:00
|
|
|
)
|
|
|
|
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
|
2020-06-28 15:51:52 +00:00
|
|
|
needLogin <- struct{}{}
|
2020-06-27 13:25:16 +00:00
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
|
2020-06-28 15:51:52 +00:00
|
|
|
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("Unable to connect to IMAP server")
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = imapClient.Login(cfg.IMAPUser, cfg.IMAPPass); err != nil {
|
|
|
|
log.WithError(err).Fatal("Unable to login to IMAP server")
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Info("IMAP connected and logged in")
|
|
|
|
|
|
|
|
if _, err = imapClient.Select(cfg.Mailbox, false); err != nil {
|
|
|
|
log.WithError(err).Fatal("Unable to select mailbox")
|
|
|
|
}
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
// Trigger re-login when log-out was received
|
|
|
|
<-imapClient.LoggedOut()
|
|
|
|
needLogin <- struct{}{}
|
|
|
|
}()
|
|
|
|
|
2020-06-27 13:25:16 +00:00
|
|
|
case <-ticker.C:
|
2020-06-28 15:51:52 +00:00
|
|
|
if _, err := imapClient.Select(cfg.Mailbox, false); err != nil {
|
|
|
|
log.WithError(err).Error("Unable to select mailbox")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-06-27 13:25:16 +00:00
|
|
|
seq, err := imap.ParseSeqSet(fmt.Sprintf("%d:*", store.LastUID+1))
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Error("Unable to parse sequence set")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
ids, err := imapClient.UidSearch(&imap.SearchCriteria{
|
|
|
|
Uid: seq,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Error("Unable to search for messages")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(ids) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
var tmpMsg = make(chan *imap.Message)
|
|
|
|
go func() {
|
|
|
|
for msg := range tmpMsg {
|
|
|
|
if msg.Uid <= store.LastUID {
|
|
|
|
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("Unable to fetch 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("Unable to parse 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) {
|
|
|
|
if err := hdl.Process(imapClient, msg, mail); err != nil {
|
|
|
|
log.WithError(err).Error("Error while processing message")
|
|
|
|
}
|
|
|
|
}(msg)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Mark message as processed in store
|
|
|
|
if msg.Uid > store.LastUID {
|
|
|
|
store.LastUID = msg.Uid
|
|
|
|
if err = store.saveStorage(); err != nil {
|
|
|
|
log.WithError(err).Error("Unable to save storage")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|