mirror of
https://github.com/Luzifer/automail.git
synced 2024-12-20 13:01:20 +00:00
Initial version
This commit is contained in:
commit
2ce834db4a
8 changed files with 606 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
automail
|
||||
config.yaml
|
||||
.env
|
||||
store.yaml
|
124
commands.go
Normal file
124
commands.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type command interface {
|
||||
Execute(*client.Client, *imap.Message, io.Writer) error
|
||||
}
|
||||
|
||||
type commandTypeWrap struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (c commandTypeWrap) rewrap(data []byte) (command, error) {
|
||||
var out command
|
||||
|
||||
switch c.Type {
|
||||
|
||||
case "move":
|
||||
out = new(commandMove)
|
||||
|
||||
case "add_flags":
|
||||
out = new(commandAddFlags)
|
||||
|
||||
case "del_flags":
|
||||
out = new(commandDelFlags)
|
||||
|
||||
case "set_flags":
|
||||
out = new(commandSetFlags)
|
||||
|
||||
default:
|
||||
return nil, errors.New("Command not found")
|
||||
|
||||
}
|
||||
|
||||
return out, errors.Wrap(json.Unmarshal(data, out), "Unable to unmarshal into command")
|
||||
}
|
||||
|
||||
type commandMove struct {
|
||||
ToMailbox string `json:"to_mailbox"`
|
||||
}
|
||||
|
||||
func (c commandMove) Execute(client *client.Client, msg *imap.Message, stdin io.Writer) error {
|
||||
s := &imap.SeqSet{}
|
||||
s.AddNum(msg.Uid)
|
||||
|
||||
if err := client.UidCopy(s, c.ToMailbox); err != nil {
|
||||
return errors.Wrap(err, "Unable to copy to target mailbox")
|
||||
}
|
||||
|
||||
return errors.Wrap(
|
||||
client.UidStore(s, imap.FormatFlagsOp(imap.AddFlags, true), []interface{}{imap.DeletedFlag}, nil),
|
||||
"Unable to set deleted flag in original mailbox",
|
||||
)
|
||||
}
|
||||
|
||||
type commandAddFlags struct {
|
||||
Flags []string `json:"flags"`
|
||||
}
|
||||
|
||||
func (c commandAddFlags) Execute(client *client.Client, msg *imap.Message, stdin io.Writer) error {
|
||||
var (
|
||||
flags []interface{}
|
||||
s = &imap.SeqSet{}
|
||||
)
|
||||
s.AddNum(msg.Uid)
|
||||
|
||||
for _, f := range c.Flags {
|
||||
flags = append(flags, f)
|
||||
}
|
||||
|
||||
return errors.Wrap(
|
||||
client.UidStore(s, imap.FormatFlagsOp(imap.AddFlags, true), flags, nil),
|
||||
"Unable to add flags",
|
||||
)
|
||||
}
|
||||
|
||||
type commandDelFlags struct {
|
||||
Flags []string `json:"flags"`
|
||||
}
|
||||
|
||||
func (c commandDelFlags) Execute(client *client.Client, msg *imap.Message, stdin io.Writer) error {
|
||||
var (
|
||||
flags []interface{}
|
||||
s = &imap.SeqSet{}
|
||||
)
|
||||
s.AddNum(msg.Uid)
|
||||
|
||||
for _, f := range c.Flags {
|
||||
flags = append(flags, f)
|
||||
}
|
||||
|
||||
return errors.Wrap(
|
||||
client.UidStore(s, imap.FormatFlagsOp(imap.RemoveFlags, true), flags, nil),
|
||||
"Unable to remove flags",
|
||||
)
|
||||
}
|
||||
|
||||
type commandSetFlags struct {
|
||||
Flags []string `json:"flags"`
|
||||
}
|
||||
|
||||
func (c commandSetFlags) Execute(client *client.Client, msg *imap.Message, stdin io.Writer) error {
|
||||
var (
|
||||
flags []interface{}
|
||||
s = &imap.SeqSet{}
|
||||
)
|
||||
s.AddNum(msg.Uid)
|
||||
|
||||
for _, f := range c.Flags {
|
||||
flags = append(flags, f)
|
||||
}
|
||||
|
||||
return errors.Wrap(
|
||||
client.UidStore(s, imap.FormatFlagsOp(imap.SetFlags, true), flags, nil),
|
||||
"Unable to set flags",
|
||||
)
|
||||
}
|
24
config.go
Normal file
24
config.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Handlers []mailHandler `yaml:"handlers"`
|
||||
}
|
||||
|
||||
func loadConfig() (*config, error) {
|
||||
var out = &config{}
|
||||
|
||||
f, err := os.Open(cfg.Config)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to open config file")
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return out, errors.Wrap(yaml.NewDecoder(f).Decode(out), "Unable to decode config")
|
||||
}
|
14
go.mod
Normal file
14
go.mod
Normal file
|
@ -0,0 +1,14 @@
|
|||
module github.com/Luzifer/automail
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/Luzifer/go_helpers/v2 v2.10.0
|
||||
github.com/Luzifer/rconfig/v2 v2.2.1
|
||||
github.com/emersion/go-imap v1.0.5
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20200515024757-02f0bf5dbca3
|
||||
github.com/jhillyerd/enmime v0.8.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
)
|
63
go.sum
Normal file
63
go.sum
Normal file
|
@ -0,0 +1,63 @@
|
|||
github.com/Luzifer/go_helpers v1.4.0 h1:Pmm058SbYewfnpP1CHda/zERoAqYoZFiBHF4l8k03Ko=
|
||||
github.com/Luzifer/go_helpers/v2 v2.10.0 h1:rA3945P6tH1PKRdcVD+nAdAWojfgwX8wQm/jjUNPmfg=
|
||||
github.com/Luzifer/go_helpers/v2 v2.10.0/go.mod h1:ZnWxPjyCdQ4rZP3kNiMSUW/7FigU1X9Rz8XopdJ5ZCU=
|
||||
github.com/Luzifer/rconfig v1.2.0 h1:waD1sqasGVSQSrExpLrQ9Q1JmMaltrS391VdOjWXP/I=
|
||||
github.com/Luzifer/rconfig/v2 v2.2.1 h1:zcDdLQlnlzwcBJ8E0WFzOkQE1pCMn3EbX0dFYkeTczg=
|
||||
github.com/Luzifer/rconfig/v2 v2.2.1/go.mod h1:OKIX0/JRZrPJ/ZXXWklQEFXA6tBfWaljZbW37w+sqBw=
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emersion/go-imap v1.0.5 h1:8xg/d2wo2BBP3AEP5AOaM/6i8887RGyVW2st/IVHWUw=
|
||||
github.com/emersion/go-imap v1.0.5/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
|
||||
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs=
|
||||
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20200515024757-02f0bf5dbca3 h1:k3/6a1Shi7GGCp9QpyYuXsMM6ncTOjCzOE9Fd6CDA+Q=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20200515024757-02f0bf5dbca3/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43 h1:jTkyeF7NZ5oIr0ESmcrpiDgAfoidCBF4F5kJhjtaRwE=
|
||||
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jhillyerd/enmime v0.8.1 h1:Kz4xj3sJJ4Ju8e+w/7v9H4Matv5ijPgv7UkhPf+C15I=
|
||||
github.com/jhillyerd/enmime v0.8.1/go.mod h1:MBHs3ugk03NGjMM6PuRynlKf+HA5eSillZ+TRCm73AE=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/leekchan/gtf v0.0.0-20190214083521-5fba33c5b00b/go.mod h1:thNruaSwydMhkQ8dXzapABF9Sc1Tz08ZBcDdgott9RA=
|
||||
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
|
||||
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
|
||||
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19 h1:WB265cn5OpO+hK3pikC9hpP1zI/KTwmyMFKloW9eOVc=
|
||||
gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
156
handler.go
Normal file
156
handler.go
Normal file
|
@ -0,0 +1,156 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/jhillyerd/enmime"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type mailTransport struct {
|
||||
Attachments []string `json:"attachments"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
HTML string `json:"html"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func mailToTransport(msg *enmime.Envelope) *mailTransport {
|
||||
var out = &mailTransport{
|
||||
Headers: map[string]string{},
|
||||
HTML: msg.HTML,
|
||||
Text: msg.Text,
|
||||
}
|
||||
|
||||
for _, a := range msg.Attachments {
|
||||
out.Attachments = append(out.Attachments, a.FileName)
|
||||
}
|
||||
|
||||
for _, hn := range msg.GetHeaderKeys() {
|
||||
out.Headers[hn] = msg.GetHeader(hn)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
type mailHandler struct {
|
||||
Match []matcher `yaml:"match"`
|
||||
Command []string `yaml:"command"`
|
||||
}
|
||||
|
||||
func (m mailHandler) Handles(msg *enmime.Envelope) bool {
|
||||
for _, ma := range m.Match {
|
||||
if ma.Match(msg) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m mailHandler) Process(imapClient *client.Client, msg *imap.Message, envelope *enmime.Envelope) error {
|
||||
cmd := exec.Command(m.Command[0], m.Command[1:]...)
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to create stdin pipe")
|
||||
}
|
||||
defer stdin.Close()
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to create stdout pipe")
|
||||
}
|
||||
defer stdout.Close()
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
if scanner.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var cw = new(commandTypeWrap)
|
||||
if err := json.Unmarshal(scanner.Bytes(), cw); err != nil {
|
||||
log.WithError(err).Error("Unable to unmarshal command")
|
||||
continue
|
||||
}
|
||||
|
||||
c, err := cw.rewrap(scanner.Bytes())
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Unable to parse command")
|
||||
continue
|
||||
}
|
||||
|
||||
if err = c.Execute(imapClient, msg, stdin); err != nil {
|
||||
log.WithError(err).Error("Unable to execute command")
|
||||
continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
return errors.Wrap(err, "Unable to start process")
|
||||
}
|
||||
|
||||
if err = json.NewEncoder(stdin).Encode(mailToTransport(envelope)); err != nil {
|
||||
return errors.Wrap(err, "Unable to send mail to process")
|
||||
}
|
||||
|
||||
return errors.Wrap(cmd.Wait(), "Process exited unclean")
|
||||
}
|
||||
|
||||
type matcher struct {
|
||||
Any bool `yaml:"any"`
|
||||
Header string `yaml:"header"`
|
||||
Exact *string `yaml:"exact"`
|
||||
Includes *string `yaml:"includes"`
|
||||
RegExp *string `yaml:"regexp"`
|
||||
}
|
||||
|
||||
func (m matcher) Match(msg *enmime.Envelope) bool {
|
||||
if m.Any {
|
||||
return true
|
||||
}
|
||||
|
||||
switch strings.ToLower(m.Header) {
|
||||
|
||||
case "cc":
|
||||
return m.matchString(msg.GetHeader("cc"))
|
||||
|
||||
case "from":
|
||||
return m.matchString(msg.GetHeader("from"))
|
||||
|
||||
case "subject":
|
||||
return m.matchString(msg.GetHeader("subject"))
|
||||
|
||||
case "to":
|
||||
return m.matchString(msg.GetHeader("to"))
|
||||
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m matcher) matchString(s string) bool {
|
||||
if m.Exact != nil && s == *m.Exact {
|
||||
return true
|
||||
}
|
||||
|
||||
if m.Includes != nil && strings.Contains(s, *m.Includes) {
|
||||
return true
|
||||
}
|
||||
|
||||
if m.RegExp != nil && regexp.MustCompile(*m.RegExp).MatchString(s) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
178
main.go
Normal file
178
main.go
Normal file
|
@ -0,0 +1,178 @@
|
|||
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")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
defer imapClient.Close()
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
var (
|
||||
messages = make(chan *imap.Message, 1000)
|
||||
sigs = make(chan os.Signal)
|
||||
ticker = time.NewTicker(cfg.FetchInterval)
|
||||
)
|
||||
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
for {
|
||||
select {
|
||||
|
||||
case <-ticker.C:
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
43
store.go
Normal file
43
store.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type storage struct {
|
||||
LastUID uint32
|
||||
}
|
||||
|
||||
func loadStorage() (*storage, error) {
|
||||
var out = &storage{}
|
||||
|
||||
if _, err := os.Stat(cfg.StorageFile); os.IsNotExist(err) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
f, err := os.Open(cfg.StorageFile)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to open storage file")
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return out, errors.Wrap(yaml.NewDecoder(f).Decode(out), "Unable to decode storage file")
|
||||
}
|
||||
|
||||
func (s storage) saveStorage() error {
|
||||
if err := os.MkdirAll(path.Dir(cfg.StorageFile), 0700); err != nil {
|
||||
return errors.Wrap(err, "Unable to ensure directory for storage file")
|
||||
}
|
||||
|
||||
f, err := os.Create(cfg.StorageFile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to create storage file")
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return errors.Wrap(yaml.NewEncoder(f).Encode(s), "Unable to encode storage file")
|
||||
}
|
Loading…
Reference in a new issue