1
0
Fork 0
mirror of https://github.com/Luzifer/automail.git synced 2024-12-20 13:01:20 +00:00

Initial version

This commit is contained in:
Knut Ahlers 2020-06-27 15:25:16 +02:00
commit 2ce834db4a
Signed by: luzifer
GPG key ID: DC2729FDD34BE99E
8 changed files with 606 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
automail
config.yaml
.env
store.yaml

124
commands.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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")
}