aboutsummaryrefslogtreecommitdiffstats
path: root/common
diff options
context:
space:
mode:
authorClemens Fries <github-clockrotz@xenoworld.de>2016-11-01 17:11:58 +0100
committerClemens Fries <github-clockrotz@xenoworld.de>2016-11-01 17:11:58 +0100
commit533681e0f83ec4f69b3b8e9f1982ed9f089285b4 (patch)
tree4792c3b3e6b353798f8564ff90d6572081755e1f /common
Initial commit
Diffstat (limited to 'common')
-rw-r--r--common/configuration.go142
-rw-r--r--common/configuration_test.go105
-rw-r--r--common/consts.go49
-rw-r--r--common/message.go231
-rw-r--r--common/message_test.go82
-rw-r--r--common/util.go63
-rw-r--r--common/util_test.go79
7 files changed, 751 insertions, 0 deletions
diff --git a/common/configuration.go b/common/configuration.go
new file mode 100644
index 0000000..2948116
--- /dev/null
+++ b/common/configuration.go
@@ -0,0 +1,142 @@
+/* configuration.go: module for managing message properties
+ *
+ * Copyright (C) 2016 Clemens Fries <github-clockrotz@xenoworld.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package common
+
+import (
+ "fmt"
+ "github.com/go-ini/ini"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+type Configuration struct {
+ Data map[string]string
+}
+
+func NewConfiguration() *Configuration {
+ return &Configuration{
+ Data: map[string]string{},
+ }
+}
+
+func (c *Configuration) Get(key string) string {
+ return c.Data[key]
+}
+
+func (c *Configuration) Set(key string, value string) {
+ c.Data[key] = value
+}
+
+// Load configuration from an array of strings in the form `key: value`.
+func (c *Configuration) Load(text []string) {
+ c.Data = map[string]string{}
+
+ for _, line := range text {
+ r := strings.SplitN(line, ":", 2)
+
+ key := strings.TrimSpace(r[0])
+
+ if len(r) == 2 {
+ c.Data[key] = strings.TrimSpace(r[1])
+ } else {
+ if key != "" {
+ c.Data[key] = ""
+ }
+ }
+ }
+}
+
+// Merge the `src` configuration into this configuration.
+func (c *Configuration) MergeWith(src *Configuration) {
+ for k, v := range (*src).Data {
+ (*c).Data[k] = v
+ }
+}
+
+// Merge arguments from a INI section into the given map.
+func mergeIniSection(section *ini.Section, dst *map[string]string) {
+ for _, k := range section.KeyStrings() {
+ (*dst)[k] = section.Key(k).String()
+ }
+}
+
+// Merges the "default" and the "cmd" section from CONF_CONFIG_FILENAME.
+func (c *Configuration) MergeWithIni(cmd string) {
+ cfg, err := ini.Load(c.Get(CONF_CONFIG_FILENAME))
+
+ if err == nil {
+ section, err := cfg.GetSection("default")
+
+ if err == nil {
+ mergeIniSection(section, &c.Data)
+ }
+
+ section, err = cfg.GetSection(cmd)
+
+ if err == nil {
+ mergeIniSection(section, &c.Data)
+ }
+ }
+}
+
+// Merge arguments from the DocOpt parser into a configuration map. All
+// arguments that are not `nil` and start with "--" will be merged.
+// Booleans will be converted to strings.
+func (c *Configuration) MergeWithDocOptArgs(cmd string, args *map[string]interface{}) {
+ for k, v := range *args {
+
+ // The args list contains the name of the command, but we are not
+ // interested in it.
+ if k == cmd {
+ continue
+ }
+
+ // Merge all args that start with -- and where the value is not nil
+ if v != nil && (len(k) > 2 && k[0:2] == "--") {
+ switch v.(type) {
+ case string:
+ c.Data[k[2:]] = v.(string)
+ case bool:
+ c.Data[k[2:]] = strconv.FormatBool(v.(bool))
+ }
+
+ }
+ }
+}
+
+// Dump the configuration as strings in the form `key: value`.
+func (c *Configuration) DumpConfig() []string {
+ keys := make([]string, len(c.Data))
+
+ i := 0
+ for k, _ := range c.Data {
+ keys[i] = k
+ i++
+ }
+
+ sort.Strings(keys)
+
+ result := make([]string, len(keys))
+
+ for i, k := range keys {
+ result[i] = fmt.Sprintf("%s: %s", k, c.Get(k))
+ }
+
+ return result
+}
diff --git a/common/configuration_test.go b/common/configuration_test.go
new file mode 100644
index 0000000..dbf2697
--- /dev/null
+++ b/common/configuration_test.go
@@ -0,0 +1,105 @@
+/* configuration_test.go: unit tests for the configuration module
+ *
+ * Copyright (C) 2016 Clemens Fries <github-clockrotz@xenoworld.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package common
+
+import (
+ "github.com/go-ini/ini"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestConfiguration_MergeWith(t *testing.T) {
+ src := Configuration{
+ Data: map[string]string{
+ "port": "1",
+ "server": "example.com",
+ },
+ }
+
+ dst := Configuration{
+ Data: map[string]string{
+ "server": "example.net",
+ "workdir": "/home/foo",
+ },
+ }
+
+ expected := map[string]string{
+ "port": "1",
+ "server": "example.com",
+ "workdir": "/home/foo",
+ }
+
+ dst.MergeWith(&src)
+
+ assert.Equal(t, expected, dst.Data)
+}
+
+func TestConfiguration_MergeWithIni(t *testing.T) {
+ dst := map[string]string{
+ "port": "1",
+ "server": "example.com",
+ }
+
+ iniContents := []byte(`
+ [foo]
+ port = 1234
+ workdir = /home/foo
+ `)
+
+ expected := map[string]string{
+ "port": "1234",
+ "server": "example.com",
+ "workdir": "/home/foo",
+ }
+
+ cfg, _ := ini.Load(iniContents)
+
+ section := cfg.Section("foo")
+
+ mergeIniSection(section, &dst)
+
+ assert.Equal(t, expected, dst)
+}
+
+func TestConfiguration_Load(t *testing.T) {
+ text := []string{
+ "to: me@example.com",
+ "from: ",
+ "subject",
+ "",
+ }
+
+ expected := map[string]string{
+ "to": "me@example.com",
+ "from": "",
+ "subject": "",
+ }
+
+ conf := Configuration{}
+ conf.Load(text)
+
+ assert.Equal(t, expected, conf.Data)
+}
+
+func TestConfiguration_Get(t *testing.T) {
+ conf := NewConfiguration()
+
+ conf.Set("foo", "bar")
+
+ assert.Equal(t, "bar", conf.Get("foo"))
+}
diff --git a/common/consts.go b/common/consts.go
new file mode 100644
index 0000000..822f413
--- /dev/null
+++ b/common/consts.go
@@ -0,0 +1,49 @@
+/* consts.go: global constants
+ *
+ * Copyright (C) 2016 Clemens Fries <github-clockrotz@xenoworld.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package common
+
+// Project wide constants.
+const (
+ TIME_FORMAT = "15:04"
+ DATE_FORMAT = "2006-01-02"
+ DATETIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT
+
+ DIR_TODO = "todo"
+ DIR_DRAFTS = "drafts"
+ DIR_ERRORS = "errors"
+ DIR_DONE = "done"
+
+ CMD_USAGE = "usage"
+ CMD_RUN = "run"
+
+ CONF_WORKDIR = "workdir"
+ CONF_DATE = "date"
+ CONF_DAYS = "days"
+ CONF_SUBJECT = "subject"
+ CONF_TO = "to"
+ CONF_FROM = "from"
+ CONF_REPLY_TO = "reply-to"
+ CONF_CC = "cc"
+ CONF_BCC = "bcc"
+ CONF_CONFIG_FILENAME = "config"
+ CONF_SMTP_SERVER = "server"
+ CONF_SMTP_PORT = "port"
+ CONF_SMTP_INSECURE = "insecure"
+ CONF_NOT_BEFORE = "not-before"
+ CONF_NOT_AFTER = "not-after"
+)
diff --git a/common/message.go b/common/message.go
new file mode 100644
index 0000000..fbea53d
--- /dev/null
+++ b/common/message.go
@@ -0,0 +1,231 @@
+/* message.go: module for managing message information
+ *
+ * Copyright (C) 2016 Clemens Fries <github-clockrotz@xenoworld.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package common
+
+import (
+ "bufio"
+ "fmt"
+ "net/mail"
+ "os"
+ "path/filepath"
+)
+
+type Message struct {
+ Conf Configuration
+ Body []string
+ Name string
+}
+
+// Supporting sort.Interface.
+type Messages []Message
+
+// Load all messages in the given directory.
+func NewMessagesFromDirectory(dir string) Messages {
+ messages := []Message{}
+
+ err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ // Only interested in regular files
+ if !info.Mode().IsRegular() {
+ return nil
+ }
+
+ // Parse only .msg files
+ if filepath.Ext(info.Name()) != ".msg" {
+ return nil
+ }
+
+ message, err := NewMessageFromFile(path)
+
+ if err != nil {
+ return err
+ }
+
+ messages = append(messages, message)
+
+ return nil
+ })
+
+ // TODO: Maybe return error?
+ if err != nil {
+ fmt.Printf("Error while reading messages from %s:\n\t%s\n", dir, err.Error())
+ }
+
+ return messages
+}
+
+func (m Messages) Len() int { return len(m) }
+func (m Messages) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
+func (m Messages) Less(i, j int) bool {
+ // TODO: We are being very confident here w.r.t. errors
+ t1, _ := ParseTime(m[i].Get(CONF_DATE))
+ t2, _ := ParseTime(m[j].Get(CONF_DATE))
+
+ return t1.Before(t2)
+}
+
+// Create a new, empty Message.
+func NewMessage() *Message {
+ return &Message{
+ Conf: *NewConfiguration(),
+ Body: []string{},
+ Name: "",
+ }
+}
+
+// Construct new Message from the given file.
+func NewMessageFromFile(path string) (Message, error) {
+ f, err := os.Open(path)
+
+ if err != nil {
+ return Message{}, err
+ }
+
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+
+ lines := []string{}
+
+ for scanner.Scan() {
+ lines = append(lines, scanner.Text())
+ }
+
+ msgConf, msgBody := SplitMessage(lines)
+
+ configuration := Configuration{}
+ configuration.Load(msgConf)
+
+ return Message{
+ Body: msgBody,
+ Conf: configuration,
+ Name: filepath.Base(path),
+ }, nil
+}
+
+// Write a message to a file, such that it could be loaded again.
+func (m *Message) WriteToFile(file string) error {
+ f, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY, 0666)
+
+ if err != nil {
+ return err
+ }
+
+ defer f.Close()
+
+ for _, s := range m.Conf.DumpConfig() {
+ f.WriteString(s)
+ f.WriteString("\n")
+ }
+
+ f.WriteString("\n")
+
+ for _, s := range m.Body {
+ f.WriteString(s)
+ f.WriteString("\n")
+ }
+
+ return nil
+}
+
+// Return the specified configuration key's value.
+func (m *Message) Get(key string) string {
+ return m.Conf.Data[key]
+}
+
+// Checks if set and if ParseAddressList succeeds
+func verifyAddressList(addresses string) error {
+ if addresses == "" {
+ // TODO: Why is this part different than the one in verifyAddress()? Maybe not intentional?
+ return nil
+ }
+
+ _, err := mail.ParseAddressList(addresses)
+
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// Checks if no nil and if ParseAddress succeeds.
+func verifyAddress(paramName string, address string) error {
+ if address == "" {
+ return fmt.Errorf("'%s' parameter is missing", paramName)
+ }
+
+ _, err := mail.ParseAddress(address)
+
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// Verify if a message has all necessary parameters. We need at least to, from,
+// subject, and date. This will also verify optional address lists, etc.
+func (m *Message) Verify() []error {
+ errors := []error{}
+
+ if err := verifyAddress("from", m.Get("from")); err != nil {
+ errors = append(errors, err)
+ }
+
+ if err := verifyAddress("to", m.Get("to")); err != nil {
+ errors = append(errors, err)
+ }
+
+ if m.Get("subject") == "" {
+ errors = append(errors, fmt.Errorf("'subject' parameter is missing"))
+ }
+
+ if m.Get("date") == "" {
+ errors = append(errors, fmt.Errorf("'date' parameter is missing"))
+ } else {
+ _, err := ParseTime(m.Get("date"))
+
+ if err != nil {
+ errors = append(errors, fmt.Errorf("'date' format error: %s", err.Error()))
+ }
+ }
+
+ if m.Get("reply-to") != "" {
+ if err := verifyAddress("nil", m.Get("to")); err != nil {
+ errors = append(errors, err)
+ }
+ }
+
+ if err := verifyAddressList(m.Get("cc")); err != nil {
+ errors = append(errors, err)
+ }
+
+ if err := verifyAddressList(m.Get("bcc")); err != nil {
+ errors = append(errors, err)
+ }
+
+ if len(errors) == 0 {
+ return nil
+ }
+
+ return errors
+}
diff --git a/common/message_test.go b/common/message_test.go
new file mode 100644
index 0000000..74ed09f
--- /dev/null
+++ b/common/message_test.go
@@ -0,0 +1,82 @@
+/* message_test.go: unit tests for the message module
+ *
+ * Copyright (C) 2016 Clemens Fries <github-clockrotz@xenoworld.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package common
+
+import (
+ "github.com/stretchr/testify/assert"
+ "path/filepath"
+ "sort"
+ "testing"
+)
+
+func TestReadMessagesAndVerify(t *testing.T) {
+ workdir := "testdata"
+
+ conf := NewConfiguration()
+ conf.Set("from", "me@example.com")
+
+ messages := NewMessagesFromDirectory(filepath.Join(workdir, "todo"))
+
+ for _, message := range messages {
+ // If we don't to this, it will fail because "from" is missing
+ message.Conf.MergeWith(conf)
+
+ if err := message.Verify(); err != nil {
+ t.Errorf("Verification of message '%s' failed with %s", message.Name, err)
+ }
+ }
+}
+
+func TestMessage_Get(t *testing.T) {
+ message := Message{Conf: *NewConfiguration()}
+
+ message.Conf.Set("foo", "bar")
+
+ assert.Equal(t, "bar", message.Get("foo"))
+}
+
+func TestMessagesSort(t *testing.T) {
+ messages := []Message{
+ {
+ Name: "1",
+ Conf: Configuration{
+ Data: map[string]string{
+ "date": "2000-01-01",
+ }}},
+ {
+ Name: "2",
+ Conf: Configuration{
+ Data: map[string]string{
+ "date": "2010-01-01",
+ }}},
+ {
+ Name: "3",
+ Conf: Configuration{
+ Data: map[string]string{
+ "date": "2005-01-01",
+ }}},
+ }
+
+ sort.Sort(Messages(messages))
+
+ for i, k := range []string{"1", "3", "2"} {
+ if messages[i].Name != k {
+ t.Errorf("expected: %s, but was %s", k, messages[i].Name)
+ }
+ }
+}
diff --git a/common/util.go b/common/util.go
new file mode 100644
index 0000000..a4422c7
--- /dev/null
+++ b/common/util.go
@@ -0,0 +1,63 @@
+/* util.go: various utility functions
+ *
+ * Copyright (C) 2016 Clemens Fries <github-clockrotz@xenoworld.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package common
+
+import (
+ "strings"
+ "time"
+)
+
+// Split a text in two parts on the first blank line.
+func SplitMessage(text []string) ([]string, []string) {
+ conf := []string{}
+ body := []string{}
+
+ inBody := false
+
+ for _, line := range text {
+ if !inBody && strings.TrimSpace(line) == "" {
+ inBody = true
+ continue // skip the separating line
+ }
+
+ if inBody {
+ body = append(body, line)
+ } else {
+ conf = append(conf, line)
+ }
+ }
+
+ return conf, body
+}
+
+// Parse a time which may either be just a date or a date with time.
+func ParseTime(datetime string) (time.Time, error) {
+ result, err := time.ParseInLocation(DATE_FORMAT, datetime, time.Now().Location())
+
+ if err == nil {
+ return result, nil
+ }
+
+ result, err = time.ParseInLocation(DATETIME_FORMAT, datetime, time.Now().Location())
+
+ if err == nil {
+ return result, nil
+ }
+
+ return result, err
+}
diff --git a/common/util_test.go b/common/util_test.go
new file mode 100644
index 0000000..201cfe0
--- /dev/null
+++ b/common/util_test.go
@@ -0,0 +1,79 @@
+/* util_test.go: unit tests for utility functions
+ *
+ * Copyright (C) 2016 Clemens Fries <github-clockrotz@xenoworld.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package common
+
+import (
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestSplitMessage(t *testing.T) {
+ message := []string{
+ "to: me@example.com",
+ "subject: foo",
+ "",
+ "Dear Me,",
+ "",
+ "have a nice day.",
+ }
+
+ conf, body := SplitMessage(message)
+
+ expectedConf := []string{
+ "to: me@example.com",
+ "subject: foo",
+ }
+
+ expectedBody := []string{
+ "Dear Me,",
+ "",
+ "have a nice day.",
+ }
+
+ assert.Equal(t, expectedConf, conf)
+ assert.Equal(t, expectedBody, body)
+}
+
+func TestParseTime(t *testing.T) {
+ d1 := "2061-07-28"
+ d2 := "2061-07-28 12:23"
+ invalid1 := "2061/07/28"
+ invalid2 := "2061-07-28 12:23:00"
+
+ r, err := ParseTime(d1)
+ assert.Nil(t, err)
+
+ if r.Hour() != 0 || r.Minute() != 0 {
+ t.Errorf("expected Hour and Minute to be 0, but was %s", r)
+ }
+
+ r, err = ParseTime(d2)
+ assert.Nil(t, err)
+
+ if r.Hour() != 12 || r.Minute() != 23 {
+ t.Errorf("expected Hour and Minute to be 12:23, but was %s", r)
+ }
+
+ // Expected to fail
+ _, err = ParseTime(invalid1)
+ assert.NotNil(t, err)
+
+ // Expected to fail
+ _, err = ParseTime(invalid2)
+ assert.NotNil(t, err)
+}