aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--go.mod1
-rw-r--r--internal/config/config.go94
-rw-r--r--internal/feed/cache.go42
-rw-r--r--internal/feed/feed.go80
-rw-r--r--internal/feed/mail.go2
-rw-r--r--internal/feed/parse.go18
-rw-r--r--internal/feed/state.go76
-rw-r--r--internal/feed/template/template.go2
-rw-r--r--internal/imap/client.go12
-rw-r--r--internal/imap/connection.go4
-rw-r--r--internal/imap/imap.go4
-rw-r--r--internal/imap/url.go4
-rw-r--r--internal/yaml/yaml.go138
-rw-r--r--main.go17
-rw-r--r--pkg/config/config.go137
-rw-r--r--pkg/config/feed.go38
-rw-r--r--pkg/config/yaml.go120
-rw-r--r--pkg/config/yaml_test.go (renamed from internal/yaml/yaml_test.go)143
-rw-r--r--pkg/log/log.go (renamed from internal/log/log.go)0
-rw-r--r--pkg/util/util.go (renamed from internal/util/util.go)0
20 files changed, 495 insertions, 437 deletions
diff --git a/go.mod b/go.mod
index 7b05b3b..021c704 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/Necoro/feed2imap-go
go 1.14
require (
+ github.com/davecgh/go-spew v1.1.1
github.com/emersion/go-imap v1.0.4
github.com/emersion/go-message v0.11.3-0.20200422153910-8c6ac6b57e3d
github.com/mmcdole/gofeed v1.0.0-beta2.0.20200331235650-4298e4366be3
diff --git a/internal/config/config.go b/internal/config/config.go
deleted file mode 100644
index 6c74c2e..0000000
--- a/internal/config/config.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package config
-
-import (
- "fmt"
- "os"
- "os/user"
- "runtime"
- "runtime/debug"
- "strings"
-)
-
-type Map map[string]interface{}
-
-type GlobalOptions struct {
- Timeout int `yaml:"timeout"`
- DefaultEmail string `yaml:"default-email"`
- Target string `yaml:"target"`
- Parts []string `yaml:"parts"`
-}
-
-var DefaultGlobalOptions = GlobalOptions{
- Timeout: 30,
- DefaultEmail: username() + "@" + hostname(),
- Target: "",
- Parts: []string{"text", "html"},
-}
-
-type Config struct {
- GlobalOptions
- GlobalConfig Map
-}
-
-type Options struct {
- MinFreq int `yaml:"min-frequency"`
- InclImages *bool `yaml:"include-images"`
-}
-
-func (c *Config) Validate() error {
- if c.Target == "" {
- return fmt.Errorf("No target set!")
- }
-
- return nil
-}
-
-func (c *Config) WithPartText() bool {
- for _, part := range c.Parts {
- if part == "text" {
- return true
- }
- }
-
- return false
-}
-
-func (c *Config) WithPartHtml() bool {
- for _, part := range c.Parts {
- if part == "html" {
- return true
- }
- }
-
- return false
-}
-
-func Version() string {
- bi, ok := debug.ReadBuildInfo()
- if !ok {
- return "(unknown)"
- }
- return bi.Main.Version
-}
-
-func hostname() (hostname string) {
- hostname, err := os.Hostname()
- if err != nil {
- hostname = "localhost"
- }
- return
-}
-
-func username() string {
- u, err := user.Current()
- switch {
- case err != nil:
- return "user"
- case runtime.GOOS == "windows":
- // the domain is attached -- remove it again
- split := strings.Split(u.Username, "\\")
- return split[len(split)-1]
- default:
- return u.Username
- }
-}
diff --git a/internal/feed/cache.go b/internal/feed/cache.go
index 4f27144..411ed47 100644
--- a/internal/feed/cache.go
+++ b/internal/feed/cache.go
@@ -9,7 +9,7 @@ import (
"os"
"time"
- "github.com/Necoro/feed2imap-go/internal/log"
+ "github.com/Necoro/feed2imap-go/pkg/log"
)
const (
@@ -77,7 +77,7 @@ func (cache *v1Cache) Version() byte {
return cache.version
}
-func New() Cache {
+func newCache() Cache {
cache := v1Cache{
Ids: map[feedDescriptor]feedId{},
Feeds: map[feedId]*cachedFeed{},
@@ -90,7 +90,7 @@ func New() Cache {
func cacheForVersion(version byte) (Cache, error) {
switch version {
case 1:
- return New(), nil
+ return newCache(), nil
default:
return nil, fmt.Errorf("unknown cache version '%d'", version)
}
@@ -114,20 +114,20 @@ func (cache *v1Cache) findItem(feed *Feed) CachedFeed {
return feed.cached.(*cachedFeed)
}
- fId := feedDescriptor{Name: feed.Name, Url: feed.Url}
- id, ok := cache.Ids[fId]
+ fDescr := feed.descriptor()
+ id, ok := cache.Ids[fDescr]
if !ok {
var otherId feedDescriptor
changed := false
for otherId, id = range cache.Ids {
- if otherId.Name == fId.Name {
- log.Warnf("Feed %s seems to have changed URLs: New '%s', old '%s'. Updating.",
- fId.Name, fId.Url, otherId.Url)
+ if otherId.Name == fDescr.Name {
+ log.Warnf("Feed %s seems to have changed URLs: newCache '%s', old '%s'. Updating.",
+ fDescr.Name, fDescr.Url, otherId.Url)
changed = true
break
- } else if otherId.Url == fId.Url {
- log.Warnf("Feed with URL '%s' seems to have changed its name: New '%s', old '%s'. Updating",
- fId.Url, fId.Name, otherId.Name)
+ } else if otherId.Url == fDescr.Url {
+ log.Warnf("Feed with URL '%s' seems to have changed its name: newCache '%s', old '%s'. Updating",
+ fDescr.Url, fDescr.Name, otherId.Name)
changed = true
break
}
@@ -139,7 +139,7 @@ func (cache *v1Cache) findItem(feed *Feed) CachedFeed {
cache.NextId++
}
- cache.Ids[fId] = id
+ cache.Ids[fDescr] = id
}
item := cache.getItem(id)
@@ -147,8 +147,7 @@ func (cache *v1Cache) findItem(feed *Feed) CachedFeed {
return item
}
-func (feeds *Feeds) StoreCache(fileName string) error {
- cache := feeds.cache
+func storeCache(cache Cache, fileName string) error {
if cache == nil {
return fmt.Errorf("trying to store nil cache")
}
@@ -178,25 +177,12 @@ func (feeds *Feeds) StoreCache(fileName string) error {
return nil
}
-func (feeds *Feeds) LoadCache(fileName string) error {
- cache, err := loadCache(fileName)
- if err != nil {
- return err
- }
- feeds.cache = cache
-
- for _, feed := range feeds.feeds {
- feed.cached = cache.findItem(feed)
- }
- return nil
-}
-
func loadCache(fileName string) (Cache, error) {
f, err := os.Open(fileName)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// no cache there yet -- make new
- return New(), nil
+ return newCache(), nil
}
return nil, fmt.Errorf("opening cache at '%s': %w", fileName, err)
}
diff --git a/internal/feed/feed.go b/internal/feed/feed.go
index 5af4188..c7fdd5f 100644
--- a/internal/feed/feed.go
+++ b/internal/feed/feed.go
@@ -1,22 +1,16 @@
package feed
import (
- "fmt"
- "strings"
- "sync"
"time"
"github.com/mmcdole/gofeed"
- "github.com/Necoro/feed2imap-go/internal/config"
- "github.com/Necoro/feed2imap-go/internal/log"
+ "github.com/Necoro/feed2imap-go/pkg/config"
+ "github.com/Necoro/feed2imap-go/pkg/log"
)
type Feed struct {
- Name string
- Target []string
- Url string
- config.Options
+ config.Feed
feed *gofeed.Feed
items []feeditem
cached CachedFeed
@@ -27,73 +21,15 @@ type feeditem struct {
*gofeed.Item
}
-type Feeds struct {
- feeds map[string]*Feed
- cache Cache
-}
-
-func NewFeeds() *Feeds {
- return &Feeds{
- feeds: map[string]*Feed{},
- }
-}
-
-func (feeds *Feeds) String() string {
- var b strings.Builder
- app := func(a ...interface{}) {
- _, _ = fmt.Fprint(&b, a...)
- }
- app("Feeds [")
-
- first := true
- for k, v := range feeds.feeds {
- if !first {
- app(", ")
- }
- app(`"`, k, `"`, ": ")
- if v == nil {
- app("<nil>")
- } else {
- _, _ = fmt.Fprintf(&b, "%+v", *v)
- }
- first = false
- }
- app("]")
-
- return b.String()
-}
-
-func (feeds *Feeds) Len() int {
- return len(feeds.feeds)
-}
-
-func (feeds *Feeds) Contains(name string) bool {
- _, ok := feeds.feeds[name]
- return ok
-}
-
-func (feeds *Feeds) Set(name string, feed *Feed) {
- feeds.feeds[name] = feed
-}
-
-func (feeds *Feeds) Foreach(f func(*Feed)) {
- for _, feed := range feeds.feeds {
- f(feed)
- }
-}
-
-func (feeds *Feeds) ForeachGo(goFunc func(*Feed, *sync.WaitGroup)) {
- var wg sync.WaitGroup
- wg.Add(feeds.Len())
-
- for _, feed := range feeds.feeds {
- go goFunc(feed, &wg)
+func (feed *Feed) descriptor() feedDescriptor {
+ return feedDescriptor{
+ Name: feed.Name,
+ Url: feed.Url,
}
- wg.Wait()
}
func (feed *Feed) NeedsUpdate(updateTime time.Time) bool {
- if !updateTime.IsZero() && int(time.Since(updateTime).Hours()) >= feed.MinFreq {
+ if !updateTime.IsZero() && int(time.Since(updateTime).Hours()) >= *feed.MinFreq {
log.Printf("Feed '%s' does not need updating, skipping.", feed.Name)
return false
}
diff --git a/internal/feed/mail.go b/internal/feed/mail.go
index ecc011f..d07f8cf 100644
--- a/internal/feed/mail.go
+++ b/internal/feed/mail.go
@@ -8,8 +8,8 @@ import (
"github.com/emersion/go-message/mail"
- "github.com/Necoro/feed2imap-go/internal/config"
"github.com/Necoro/feed2imap-go/internal/feed/template"
+ "github.com/Necoro/feed2imap-go/pkg/config"
)
func address(name, address string) []*mail.Address {
diff --git a/internal/feed/parse.go b/internal/feed/parse.go
index 6deebb2..afca971 100644
--- a/internal/feed/parse.go
+++ b/internal/feed/parse.go
@@ -8,7 +8,7 @@ import (
"github.com/mmcdole/gofeed"
- "github.com/Necoro/feed2imap-go/internal/log"
+ "github.com/Necoro/feed2imap-go/pkg/log"
)
func context() (ctxt.Context, ctxt.CancelFunc) {
@@ -41,19 +41,3 @@ func handleFeed(feed *Feed, group *sync.WaitGroup) {
log.Error(err)
}
}
-
-func (feeds Feeds) Parse() int {
- feeds.ForeachGo(handleFeed)
-
- ctr := 0
- for _, feed := range feeds.feeds {
- success := feed.Success()
- feed.cached.Checked(!success)
-
- if success {
- ctr++
- }
- }
-
- return ctr
-}
diff --git a/internal/feed/state.go b/internal/feed/state.go
new file mode 100644
index 0000000..9a6f836
--- /dev/null
+++ b/internal/feed/state.go
@@ -0,0 +1,76 @@
+package feed
+
+import (
+ "sync"
+
+ "github.com/Necoro/feed2imap-go/pkg/config"
+)
+
+type State struct {
+ feeds map[string]*Feed
+ cache Cache
+ cfg *config.Config
+}
+
+func (state *State) Foreach(f func(*Feed)) {
+ for _, feed := range state.feeds {
+ f(feed)
+ }
+}
+
+func (state *State) ForeachGo(goFunc func(*Feed, *sync.WaitGroup)) {
+ var wg sync.WaitGroup
+ wg.Add(len(state.feeds))
+
+ for _, feed := range state.feeds {
+ go goFunc(feed, &wg)
+ }
+ wg.Wait()
+}
+
+func (state *State) LoadCache(fileName string) error {
+ cache, err := loadCache(fileName)
+ if err != nil {
+ return err
+ }
+ state.cache = cache
+
+ for _, feed := range state.feeds {
+ feed.cached = cache.findItem(feed)
+ }
+ return nil
+}
+
+func (state *State) StoreCache(fileName string) error {
+ return storeCache(state.cache, fileName)
+}
+
+func (state *State) Fetch() int {
+ state.ForeachGo(handleFeed)
+
+ ctr := 0
+ for _, feed := range state.feeds {
+ success := feed.Success()
+ feed.cached.Checked(!success)
+
+ if success {
+ ctr++
+ }
+ }
+
+ return ctr
+}
+
+func NewState(cfg *config.Config) *State {
+ state := State{
+ feeds: map[string]*Feed{},
+ cache: nil, // loaded later on
+ cfg: cfg,
+ }
+
+ for name, parsedFeed := range cfg.Feeds {
+ state.feeds[name] = &Feed{Feed: parsedFeed}
+ }
+
+ return &state
+}
diff --git a/internal/feed/template/template.go b/internal/feed/template/template.go
index dd31f51..7871e06 100644
--- a/internal/feed/template/template.go
+++ b/internal/feed/template/template.go
@@ -6,7 +6,7 @@ import (
"strconv"
"strings"
- "github.com/Necoro/feed2imap-go/internal/log"
+ "github.com/Necoro/feed2imap-go/pkg/log"
)
func dict(v ...string) map[string]string {
diff --git a/internal/imap/client.go b/internal/imap/client.go
index 404c03e..24cc3f1 100644
--- a/internal/imap/client.go
+++ b/internal/imap/client.go
@@ -3,7 +3,7 @@ package imap
import (
imapClient "github.com/emersion/go-imap/client"
- "github.com/Necoro/feed2imap-go/internal/log"
+ "github.com/Necoro/feed2imap-go/pkg/log"
)
const numberConns = 5
@@ -16,9 +16,9 @@ type connConf struct {
type Client struct {
connConf
- mailboxes *mailboxes
- commander *commander
- connections [numberConns]*connection
+ mailboxes *mailboxes
+ commander *commander
+ connections [numberConns]*connection
nextFreeIndex int
}
@@ -37,7 +37,7 @@ func (client *Client) Disconnect() {
}
}
-func (client *Client) createConnection(c *imapClient.Client) *connection{
+func (client *Client) createConnection(c *imapClient.Client) *connection {
if client.nextFreeIndex >= len(client.connections) {
panic("Too many connections")
}
@@ -56,4 +56,4 @@ func (client *Client) createConnection(c *imapClient.Client) *connection{
func NewClient() *Client {
return &Client{mailboxes: NewMailboxes()}
-} \ No newline at end of file
+}
diff --git a/internal/imap/connection.go b/internal/imap/connection.go
index 358445b..4d209a9 100644
--- a/internal/imap/connection.go
+++ b/internal/imap/connection.go
@@ -8,8 +8,8 @@ import (
"github.com/emersion/go-imap"
imapClient "github.com/emersion/go-imap/client"
- "github.com/Necoro/feed2imap-go/internal/log"
- "github.com/Necoro/feed2imap-go/internal/util"
+ "github.com/Necoro/feed2imap-go/pkg/log"
+ "github.com/Necoro/feed2imap-go/pkg/util"
)
type connection struct {
diff --git a/internal/imap/imap.go b/internal/imap/imap.go
index f8dc7d7..8f4c50e 100644
--- a/internal/imap/imap.go
+++ b/internal/imap/imap.go
@@ -7,10 +7,10 @@ import (
imapClient "github.com/emersion/go-imap/client"
- "github.com/Necoro/feed2imap-go/internal/log"
+ "github.com/Necoro/feed2imap-go/pkg/log"
)
-func newImapClient(url *URL, forceTls bool) (*imapClient.Client,error) {
+func newImapClient(url *URL, forceTls bool) (*imapClient.Client, error) {
if forceTls {
c, err := imapClient.DialTLS(url.Host, nil)
if err != nil {
diff --git a/internal/imap/url.go b/internal/imap/url.go
index 6ffea72..90c34e6 100644
--- a/internal/imap/url.go
+++ b/internal/imap/url.go
@@ -4,7 +4,7 @@ import (
"net"
"net/url"
- "github.com/Necoro/feed2imap-go/internal/log"
+ "github.com/Necoro/feed2imap-go/pkg/log"
)
// Our own convenience wrapper
@@ -73,4 +73,4 @@ func NewUrl(url *url.URL) *URL {
u := URL{URL: url}
u.sanitizeUrl()
return &u
-} \ No newline at end of file
+}
diff --git a/internal/yaml/yaml.go b/internal/yaml/yaml.go
deleted file mode 100644
index 23a38ef..0000000
--- a/internal/yaml/yaml.go
+++ /dev/null
@@ -1,138 +0,0 @@
-package yaml
-
-import (
- "fmt"
- "io/ioutil"
-
- "gopkg.in/yaml.v3"
-
- C "github.com/Necoro/feed2imap-go/internal/config"
- F "github.com/Necoro/feed2imap-go/internal/feed"
- "github.com/Necoro/feed2imap-go/internal/log"
-)
-
-type config struct {
- C.GlobalOptions `yaml:",inline"`
- GlobalConfig C.Map `yaml:",inline"`
- Feeds []configGroupFeed
-}
-
-type group struct {
- Group string
- Feeds []configGroupFeed
-}
-
-type feed struct {
- Name string
- Url string
- C.Options `yaml:",inline"`
-}
-
-type configGroupFeed struct {
- Target *string
- Feed feed `yaml:",inline"`
- Group group `yaml:",inline"`
-}
-
-func (grpFeed *configGroupFeed) isGroup() bool {
- return grpFeed.Group.Group != ""
-}
-
-func (grpFeed *configGroupFeed) isFeed() bool {
- return grpFeed.Feed.Name != "" || grpFeed.Feed.Url != ""
-}
-
-func (grpFeed *configGroupFeed) target() string {
- if grpFeed.Target != nil {
- return *grpFeed.Target
- }
- if grpFeed.Feed.Name != "" {
- return grpFeed.Feed.Name
- }
-
- return grpFeed.Group.Group
-}
-
-func parse(buf []byte) (config, error) {
- parsedCfg := config{GlobalOptions: C.DefaultGlobalOptions}
-
- if err := yaml.Unmarshal(buf, &parsedCfg); err != nil {
- return config{}, fmt.Errorf("while unmarshalling: %w", err)
- }
- //fmt.Printf("--- parsedCfg:\n%+v\n\n", parsedCfg)
-
- return parsedCfg, nil
-}
-
-func appTarget(target []string, app string) []string {
- switch {
- case len(target) == 0 && app == "":
- return []string{}
- case len(target) == 0:
- return []string{app}
- case app == "":
- return target
- default:
- return append(target, app)
- }
-}
-
-// Parse the group structure and populate the `Target` fields in the feeds
-func buildFeeds(cfg []configGroupFeed, target []string, feeds *F.Feeds) error {
- for idx := range cfg {
- f := &cfg[idx] // cannot use `_, f := range cfg` as it returns copies(!), but we need the originals
- target := appTarget(target, f.target())
- switch {
- case f.isFeed() && f.isGroup():
- return fmt.Errorf("Entry with Target %s is both a Feed and a group", target)
-
- case f.isFeed():
- name := f.Feed.Name
- if name == "" {
- return fmt.Errorf("Unnamed feed")
- }
-
- if feeds.Contains(name) {
- return fmt.Errorf("Duplicate Feed Name '%s'", name)
- }
- feeds.Set(name, &F.Feed{
- Name: f.Feed.Name,
- Target: target,
- Url: f.Feed.Url,
- Options: f.Feed.Options,
- })
-
- case f.isGroup():
- if err := buildFeeds(f.Group.Feeds, target, feeds); err != nil {
- return err
- }
- }
- }
-
- return nil
-}
-
-func Load(path string) (*C.Config, *F.Feeds, error) {
- log.Printf("Reading configuration file '%s'", path)
-
- buf, err := ioutil.ReadFile(path)
- if err != nil {
- return nil, nil, fmt.Errorf("while reading '%s': %w", path, err)
- }
-
- var parsedCfg config
- if parsedCfg, err = parse(buf); err != nil {
- return nil, nil, err
- }
-
- feeds := F.NewFeeds()
-
- if err := buildFeeds(parsedCfg.Feeds, []string{}, feeds); err != nil {
- return nil, nil, fmt.Errorf("while parsing: %w", err)
- }
-
- return &C.Config{
- GlobalOptions: parsedCfg.GlobalOptions,
- GlobalConfig: parsedCfg.GlobalConfig,
- }, feeds, nil
-}
diff --git a/main.go b/main.go
index 1850710..caeafd7 100644
--- a/main.go
+++ b/main.go
@@ -7,11 +7,10 @@ import (
"os"
"sync"
- "github.com/Necoro/feed2imap-go/internal/config"
"github.com/Necoro/feed2imap-go/internal/feed"
"github.com/Necoro/feed2imap-go/internal/imap"
- "github.com/Necoro/feed2imap-go/internal/log"
- "github.com/Necoro/feed2imap-go/internal/yaml"
+ "github.com/Necoro/feed2imap-go/pkg/config"
+ "github.com/Necoro/feed2imap-go/pkg/log"
)
var cfgFile = flag.String("f", "config.yml", "configuration file")
@@ -51,7 +50,7 @@ func run() error {
log.Print("Starting up...")
- cfg, feeds, err := yaml.Load(*cfgFile)
+ cfg, err := config.Load(*cfgFile)
if err != nil {
return err
}
@@ -60,12 +59,14 @@ func run() error {
return fmt.Errorf("Configuration invalid: %w", err)
}
- err = feeds.LoadCache(*cacheFile)
+ state := feed.NewState(cfg)
+
+ err = state.LoadCache(*cacheFile)
if err != nil {
return err
}
- if success := feeds.Parse(); success == 0 {
+ if success := state.Fetch(); success == 0 {
return fmt.Errorf("No successfull feed fetch.")
}
@@ -81,11 +82,11 @@ func run() error {
defer c.Disconnect()
- feeds.ForeachGo(func(f *feed.Feed, wg *sync.WaitGroup) {
+ state.ForeachGo(func(f *feed.Feed, wg *sync.WaitGroup) {
processFeed(f, cfg, c, wg)
})
- if err = feeds.StoreCache(*cacheFile); err != nil {
+ if err = state.StoreCache(*cacheFile); err != nil {
return err
}
diff --git a/pkg/config/config.go b/pkg/config/config.go
new file mode 100644
index 0000000..83c952f
--- /dev/null
+++ b/pkg/config/config.go
@@ -0,0 +1,137 @@
+package config
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/user"
+ "runtime"
+ "runtime/debug"
+ "strings"
+
+ "github.com/Necoro/feed2imap-go/pkg/log"
+ "github.com/Necoro/feed2imap-go/pkg/util"
+)
+
+// Convenience type for the non-mapped configuration options
+// Mostly used for legacy options
+type Map map[string]interface{}
+
+// Global options, not feed specific
+type GlobalOptions struct {
+ Timeout int `yaml:"timeout"`
+ DefaultEmail string `yaml:"default-email"`
+ Target string `yaml:"target"`
+ Parts []string `yaml:"parts"`
+}
+
+// Default global options
+var DefaultGlobalOptions = GlobalOptions{
+ Timeout: 30,
+ DefaultEmail: username() + "@" + hostname(),
+ Target: "",
+ Parts: []string{"text", "html"},
+}
+
+// Per feed options
+type Options struct {
+ MinFreq *int `yaml:"min-frequency"`
+ InclImages *bool `yaml:"include-images"`
+}
+
+// Default feed options
+var DefaultFeedOptions Options
+
+func init() {
+ one := 1
+ fal := false
+ DefaultFeedOptions = Options{
+ MinFreq: &one,
+ InclImages: &fal,
+ }
+}
+
+// Config holds the global configuration options and the configured feeds
+type Config struct {
+ GlobalOptions `yaml:",inline"`
+ GlobalConfig Map `yaml:",inline"`
+ FeedOptions Options `yaml:"options"`
+ Feeds Feeds `yaml:"-"`
+}
+
+// WithDefault returns a configuration initialized with default values.
+func WithDefault() *Config {
+ return &Config{
+ GlobalOptions: DefaultGlobalOptions,
+ FeedOptions: DefaultFeedOptions,
+ GlobalConfig: Map{},
+ Feeds: Feeds{},
+ }
+}
+
+// Validates the configuration against common mistakes
+func (cfg *Config) Validate() error {
+ if cfg.Target == "" {
+ return fmt.Errorf("No target set!")
+ }
+
+ return nil
+}
+
+// Marks whether 'text' part should be included in mails
+func (cfg *Config) WithPartText() bool {
+ return util.StrContains(cfg.Parts, "text")
+}
+
+// Marks whether 'html' part should be included in mails
+func (cfg *Config) WithPartHtml() bool {
+ return util.StrContains(cfg.Parts, "html")
+}
+
+// Current feed2imap version
+func Version() string {
+ bi, ok := debug.ReadBuildInfo()
+ if !ok {
+ return "(unknown)"
+ }
+ return bi.Main.Version
+}
+
+// Load configuration from file
+func Load(path string) (*Config, error) {
+ log.Printf("Reading configuration file '%s'", path)
+
+ buf, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("while reading '%s': %w", path, err)
+ }
+
+ cfg := WithDefault()
+ if err = cfg.parse(buf); err != nil {
+ return nil, fmt.Errorf("while parsing: %w", err)
+ }
+
+ return cfg, nil
+}
+
+func hostname() (hostname string) {
+ hostname, err := os.Hostname()
+ if err != nil {
+ hostname = "localhost"
+ }
+ return
+}
+
+func username() string {
+ u, err := user.Current()
+ switch {
+ case err != nil:
+ return "user"
+ case runtime.GOOS == "windows":
+ // the domain is attached -- remove it again
+ split := strings.Split(u.Username, "\\")
+ return split[len(split)-1]
+ default:
+ return u.Username
+ }
+}
diff --git a/pkg/config/feed.go b/pkg/config/feed.go
new file mode 100644
index 0000000..03494d3
--- /dev/null
+++ b/pkg/config/feed.go
@@ -0,0 +1,38 @@
+package config
+
+import (
+ "fmt"
+ "strings"
+)
+
+// One stored feed
+type Feed struct {
+ Name string
+ Target []string `yaml:"-"`
+ Url string
+ Options `yaml:",inline"`
+}
+
+// Convenience type for all feeds
+type Feeds map[string]Feed
+
+func (feeds Feeds) String() string {
+ var b strings.Builder
+ app := func(a ...interface{}) {
+ _, _ = fmt.Fprint(&b, a...)
+ }
+ app("Feeds [")
+
+ first := true
+ for k, v := range feeds {
+ if !first {
+ app(", ")
+ }
+ app(`"`, k, `"`, ": ")
+ _, _ = fmt.Fprintf(&b, "%+v", v)
+ first = false
+ }
+ app("]")
+
+ return b.String()
+}
diff --git a/pkg/config/yaml.go b/pkg/config/yaml.go
new file mode 100644
index 0000000..53d4d98
--- /dev/null
+++ b/pkg/config/yaml.go
@@ -0,0 +1,120 @@
+package config
+
+import (
+ "fmt"
+
+ "gopkg.in/yaml.v3"
+)
+
+type config struct {
+ *Config `yaml:",inline"`
+ GlobalConfig Map `yaml:",inline"` // need to be duplicated, because the Map in Config is not filled
+ Feeds []configGroupFeed
+}
+
+type group struct {
+ Group string
+ Feeds []configGroupFeed
+}
+
+type configGroupFeed struct {
+ Target *string
+ Feed Feed `yaml:",inline"`
+ Group group `yaml:",inline"`
+}
+
+func (grpFeed *configGroupFeed) isGroup() bool {
+ return grpFeed.Group.Group != ""
+}
+
+func (grpFeed *configGroupFeed) isFeed() bool {
+ return grpFeed.Feed.Name != "" || grpFeed.Feed.Url != ""
+}
+
+func (grpFeed *configGroupFeed) target() string {
+ if grpFeed.Target != nil {
+ return *grpFeed.Target
+ }
+ if grpFeed.Feed.Name != "" {
+ return grpFeed.Feed.Name
+ }
+
+ return grpFeed.Group.Group
+}
+
+func unmarshal(buf []byte, cfg *Config) (config, error) {
+ parsedCfg := config{Config: cfg}
+
+ if err := yaml.Unmarshal(buf, &parsedCfg); err != nil {
+ return config{}, err
+ }
+ //fmt.Printf("--- parsedCfg:\n%+v\n\n", parsedCfg)
+
+ if parsedCfg.GlobalConfig == nil {
+ cfg.GlobalConfig = Map{}
+ } else {
+ cfg.GlobalConfig = parsedCfg.GlobalConfig // need to copy the map explicitly
+ }
+
+ return parsedCfg, nil
+}
+
+func (cfg *Config) parse(buf []byte) error {
+ var (
+ err error
+ parsedCfg config
+ )
+
+ if parsedCfg, err = unmarshal(buf, cfg); err != nil {
+ return fmt.Errorf("while unmarshalling: %w", err)
+ }
+
+ if err := buildFeeds(parsedCfg.Feeds, []string{}, cfg.Feeds); err != nil {
+ return fmt.Errorf("while parsing: %w", err)
+ }
+
+ return nil
+}
+
+func appTarget(target []string, app string) []string {
+ switch {
+ case len(target) == 0 && app == "":
+ return []string{}
+ case len(target) == 0:
+ return []string{app}
+ case app == "":
+ return target
+ default:
+ return append(target, app)
+ }
+}
+
+// Fetch the group structure and populate the `Target` fields in the feeds
+func buildFeeds(cfg []configGroupFeed, target []string, feeds Feeds) error {
+ for _, f := range cfg {
+ target := appTarget(target, f.target())
+ switch {
+ case f.isFeed() && f.isGroup():
+ return fmt.Errorf("Entry with Target %s is both a Feed and a group", target)
+
+ case f.isFeed():
+ name := f.Feed.Name
+ if name == "" {
+ return fmt.Errorf("Unnamed feed")
+ }
+
+ if _, ok := feeds[name]; ok {
+ return fmt.Errorf("Duplicate Feed Name '%s'", name)
+ }
+ f.Feed.Target = target
+ feeds[name] = f.Feed
+
+ case f.isGroup():
+ if err := buildFeeds(f.Group.Feeds, target, feeds); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/internal/yaml/yaml_test.go b/pkg/config/yaml_test.go
index 9562ef8..501ead3 100644
--- a/internal/yaml/yaml_test.go
+++ b/pkg/config/yaml_test.go
@@ -1,14 +1,14 @@
-package yaml
+package config
import (
"reflect"
"strings"
"testing"
- C "github.com/Necoro/feed2imap-go/internal/config"
- F "github.com/Necoro/feed2imap-go/internal/feed"
+ "github.com/davecgh/go-spew/spew"
)
+func i(i int) *int { return &i }
func s(s string) *string { return &s }
func b(b bool) *bool { return &b }
func t(s string) []string {
@@ -24,121 +24,125 @@ func TestBuildFeeds(tst *testing.T) {
wantErr bool
target string
feeds []configGroupFeed
- result F.Feeds
+ result Feeds
}{
- {name: "Empty input", wantErr: false, target: "", feeds: nil, result: F.Feeds{}},
+ {name: "Empty input", wantErr: false, target: "", feeds: nil, result: Feeds{}},
{name: "Empty Feed", wantErr: true, target: "",
feeds: []configGroupFeed{
- {Target: s("foo"), Feed: feed{Url: "google.de"}},
- }, result: F.Feeds{}},
+ {Target: s("foo"), Feed: Feed{Url: "google.de"}},
+ }, result: Feeds{}},
{name: "Empty Feed", wantErr: true, target: "",
feeds: []configGroupFeed{
- {Target: nil, Feed: feed{Url: "google.de"}},
- }, result: F.Feeds{}},
+ {Target: nil, Feed: Feed{Url: "google.de"}},
+ }, result: Feeds{}},
{name: "Duplicate Feed Name", wantErr: true, target: "",
feeds: []configGroupFeed{
- {Target: nil, Feed: feed{Name: "Dup"}},
- {Target: nil, Feed: feed{Name: "Dup"}},
- }, result: F.Feeds{}},
+ {Target: nil, Feed: Feed{Name: "Dup"}},
+ {Target: nil, Feed: Feed{Name: "Dup"}},
+ }, result: Feeds{}},
{name: "Simple", wantErr: false, target: "",
feeds: []configGroupFeed{
- {Target: s("foo"), Feed: feed{Name: "muh"}},
+ {Target: s("foo"), Feed: Feed{Name: "muh"}},
},
- result: F.Feeds{"muh": &F.Feed{Name: "muh", Target: t("foo")}},
+ result: Feeds{"muh": Feed{Name: "muh", Target: t("foo")}},
},
{name: "Simple With Target", wantErr: false, target: "moep",
feeds: []configGroupFeed{
- {Target: s("foo"), Feed: feed{Name: "muh"}},
+ {Target: s("foo"), Feed: Feed{Name: "muh"}},
},
- result: F.Feeds{"muh": &F.Feed{Name: "muh", Target: t("moep.foo")}},
+ result: Feeds{"muh": Feed{Name: "muh", Target: t("moep.foo")}},
},
{name: "Simple With Nil Target", wantErr: false, target: "moep",
feeds: []configGroupFeed{
- {Target: nil, Feed: feed{Name: "muh"}},
+ {Target: nil, Feed: Feed{Name: "muh"}},
},
- result: F.Feeds{"muh": &F.Feed{Name: "muh", Target: t("moep.muh")}},
+ result: Feeds{"muh": Feed{Name: "muh", Target: t("moep.muh")}},
},
{name: "Simple With Empty Target", wantErr: false, target: "moep",
feeds: []configGroupFeed{
- {Target: s(""), Feed: feed{Name: "muh"}},
+ {Target: s(""), Feed: Feed{Name: "muh"}},
},
- result: F.Feeds{"muh": &F.Feed{Name: "muh", Target: t("moep")}},
+ result: Feeds{"muh": Feed{Name: "muh", Target: t("moep")}},
},
{name: "Multiple Feeds", wantErr: false, target: "moep",
feeds: []configGroupFeed{
- {Target: s("foo"), Feed: feed{Name: "muh"}},
- {Target: nil, Feed: feed{Name: "bar"}},
+ {Target: s("foo"), Feed: Feed{Name: "muh"}},
+ {Target: nil, Feed: Feed{Name: "bar"}},
},
- result: F.Feeds{
- "muh": &F.Feed{Name: "muh", Target: t("moep.foo")},
- "bar": &F.Feed{Name: "bar", Target: t("moep.bar")},
+ result: Feeds{
+ "muh": Feed{Name: "muh", Target: t("moep.foo")},
+ "bar": Feed{Name: "bar", Target: t("moep.bar")},
},
},
{name: "Empty Group", wantErr: false, target: "",
feeds: []configGroupFeed{
{Target: nil, Group: group{Group: "G1"}},
},
- result: F.Feeds{},
+ result: Feeds{},
},
{name: "Simple Group", wantErr: false, target: "",
feeds: []configGroupFeed{
{Target: nil, Group: group{Group: "G1", Feeds: []configGroupFeed{
- {Target: s("bar"), Feed: feed{Name: "F1"}},
- {Target: s(""), Feed: feed{Name: "F2"}},
- {Target: nil, Feed: feed{Name: "F3"}},
+ {Target: s("bar"), Feed: Feed{Name: "F1"}},
+ {Target: s(""), Feed: Feed{Name: "F2"}},
+ {Target: nil, Feed: Feed{Name: "F3"}},
}}},
},
- result: F.Feeds{
- "F1": &F.Feed{Name: "F1", Target: t("G1.bar")},
- "F2": &F.Feed{Name: "F2", Target: t("G1")},
- "F3": &F.Feed{Name: "F3", Target: t("G1.F3")},
+ result: Feeds{
+ "F1": Feed{Name: "F1", Target: t("G1.bar")},
+ "F2": Feed{Name: "F2", Target: t("G1")},
+ "F3": Feed{Name: "F3", Target: t("G1.F3")},
},
},
{name: "Nested Groups", wantErr: false, target: "",
feeds: []configGroupFeed{
{Target: nil, Group: group{Group: "G1", Feeds: []configGroupFeed{
- {Target: nil, Feed: feed{Name: "F0"}},
+ {Target: nil, Feed: Feed{Name: "F0"}},
{Target: s("bar"), Group: group{Group: "G2",
- Feeds: []configGroupFeed{{Target: nil, Feed: feed{Name: "F1"}}}}},
+ Feeds: []configGroupFeed{{Target: nil, Feed: Feed{Name: "F1"}}}}},
{Target: s(""), Group: group{Group: "G3",
- Feeds: []configGroupFeed{{Target: s("baz"), Feed: feed{Name: "F2"}}}}},
+ Feeds: []configGroupFeed{{Target: s("baz"), Feed: Feed{Name: "F2"}}}}},
{Target: nil, Group: group{Group: "G4",
- Feeds: []configGroupFeed{{Target: nil, Feed: feed{Name: "F3"}}}}},
+ Feeds: []configGroupFeed{{Target: nil, Feed: Feed{Name: "F3"}}}}},
}}},
},
- result: F.Feeds{
- "F0": &F.Feed{Name: "F0", Target: t("G1.F0")},
- "F1": &F.Feed{Name: "F1", Target: t("G1.bar.F1")},
- "F2": &F.Feed{Name: "F2", Target: t("G1.baz")},
- "F3": &F.Feed{Name: "F3", Target: t("G1.G4.F3")},
+ result: Feeds{
+ "F0": Feed{Name: "F0", Target: t("G1.F0")},
+ "F1": Feed{Name: "F1", Target: t("G1.bar.F1")},
+ "F2": Feed{Name: "F2", Target: t("G1.baz")},
+ "F3": Feed{Name: "F3", Target: t("G1.G4.F3")},
},
},
}
for _, tt := range tests {
tst.Run(tt.name, func(tst *testing.T) {
- var feeds = F.Feeds{}
+ var feeds = Feeds{}
err := buildFeeds(tt.feeds, t(tt.target), feeds)
if (err != nil) != tt.wantErr {
tst.Errorf("buildFeeds() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && !reflect.DeepEqual(feeds, tt.result) {
- tst.Errorf("buildFeeds() got = %v, want %v", feeds, tt.result)
+ tst.Errorf("buildFeeds() got: %s\nwant: %s", spew.Sdump(feeds), spew.Sdump(tt.result))
}
})
}
}
-func defaultConfig(feeds []configGroupFeed, global C.Map) config {
+func defaultConfig(feeds []configGroupFeed, global Map) config {
+ defCfg := WithDefault()
+ if global != nil {
+ defCfg.GlobalConfig = global
+ }
return config{
- GlobalOptions: C.DefaultGlobalOptions,
- GlobalConfig: global,
- Feeds: feeds,
+ Config: defCfg,
+ Feeds: feeds,
+ GlobalConfig: global,
}
}
//noinspection GoNilness,GoNilness
-func TestParse(tst *testing.T) {
+func TestUnmarshal(tst *testing.T) {
tests := []struct {
name string
inp string
@@ -149,14 +153,21 @@ func TestParse(tst *testing.T) {
inp: "", wantErr: false, config: defaultConfig(nil, nil)},
{name: "Trash", inp: "Something", wantErr: true},
{name: "Simple config",
- inp: "something: 1\nsomething_else: 2", wantErr: false, config: defaultConfig(nil, C.Map{"something": 1, "something_else": 2})},
+ inp: "something: 1\nsomething_else: 2", wantErr: false, config: defaultConfig(nil, Map{"something": 1, "something_else": 2})},
{name: "Known config",
inp: "whatever: 2\ndefault-email: foo@foobar.de\ntimeout: 60\nsomething: 1", wantErr: false, config: func() config {
- c := defaultConfig(nil, C.Map{"something": 1, "whatever": 2})
+ c := defaultConfig(nil, Map{"something": 1, "whatever": 2})
c.Timeout = 60
c.DefaultEmail = "foo@foobar.de"
return c
}()},
+ {name: "Known config with feed-options",
+ inp: "whatever: 2\ntimeout: 60\noptions:\n min-frequency: 6", wantErr: false, config: func() config {
+ c := defaultConfig(nil, Map{"whatever": 2})
+ c.Timeout = 60
+ c.FeedOptions.MinFreq = i(6)
+ return c
+ }()},
{name: "Config with feed",
inp: `
something: 1
@@ -169,14 +180,14 @@ feeds:
`,
wantErr: false,
config: defaultConfig([]configGroupFeed{
- {Target: s("bar"), Feed: feed{
+ {Target: s("bar"), Feed: Feed{
Name: "Foo",
Url: "whatever",
- Options: C.Options{
- MinFreq: 0,
+ Options: Options{
+ MinFreq: nil,
InclImages: b(true),
},
- }}}, C.Map{"something": 1})},
+ }}}, Map{"something": 1})},
{name: "Feeds",
inp: `
@@ -191,19 +202,19 @@ feeds:
`,
wantErr: false,
config: defaultConfig([]configGroupFeed{
- {Target: nil, Feed: feed{
+ {Target: nil, Feed: Feed{
Name: "Foo",
Url: "whatever",
- Options: C.Options{
- MinFreq: 2,
+ Options: Options{
+ MinFreq: i(2),
InclImages: nil,
},
}},
- {Target: s("bla"), Feed: feed{
+ {Target: s("bla"), Feed: Feed{
Name: "Shrubbery",
Url: "google.de",
- Options: C.Options{
- MinFreq: 0,
+ Options: Options{
+ MinFreq: nil,
InclImages: b(false),
},
}},
@@ -236,7 +247,7 @@ feeds:
`,
wantErr: false,
config: defaultConfig([]configGroupFeed{
- {Target: nil, Feed: feed{
+ {Target: nil, Feed: Feed{
Name: "Foo",
Url: "whatever",
}},
@@ -246,10 +257,10 @@ feeds:
{Target: s(""), Group: group{
Group: "G2",
Feeds: []configGroupFeed{
- {Target: nil, Feed: feed{Name: "F1", Url: "google.de"}},
+ {Target: nil, Feed: Feed{Name: "F1", Url: "google.de"}},
}},
},
- {Target: nil, Feed: feed{Name: "F2"}},
+ {Target: nil, Feed: Feed{Name: "F2"}},
{Target: nil, Group: group{Group: "G3"}},
}},
},
@@ -260,13 +271,13 @@ feeds:
for _, tt := range tests {
tst.Run(tt.name, func(tst *testing.T) {
var buf = []byte(tt.inp)
- got, err := parse(buf)
+ got, err := unmarshal(buf, WithDefault())
if (err != nil) != tt.wantErr {
tst.Errorf("parse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil && !reflect.DeepEqual(got, tt.config) {
- tst.Errorf("parse() got = %#v, want %#v", got, tt.config)
+ tst.Errorf("parse() got: %s\nwant: %s", spew.Sdump(got), spew.Sdump(tt.config))
}
})
}
diff --git a/internal/log/log.go b/pkg/log/log.go
index 0238c7e..0238c7e 100644
--- a/internal/log/log.go
+++ b/pkg/log/log.go
diff --git a/internal/util/util.go b/pkg/util/util.go
index c5472ab..c5472ab 100644
--- a/internal/util/util.go
+++ b/pkg/util/util.go