├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── lib ├── consts.go ├── message.go ├── message_test.go ├── uniqueName.go ├── utils.go └── utils_test.go ├── maildir.go └── maildir_test.go /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [published] 4 | name: Publish 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Install Go 10 | uses: actions/setup-go@v2 11 | with: 12 | go-version: 1.23.x 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | - name: Get tag 16 | id: tag 17 | uses: dawidd6/action-get-tag@v1 18 | - run: | 19 | echo "publishing version: ${{steps.tag.outputs.tag}}" 20 | MOD_NAME=$(go list -m) 21 | GOPROXY=proxy.golang.org go list -m $MOD_NAME@${{steps.tag.outputs.tag}} 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.23.x, 1.24.x] 8 | os: [ubuntu-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Test 18 | run: make test 19 | - uses: shogo82148/actions-goveralls@v1 20 | with: 21 | path-to-profile: profile.cov 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Amal Francis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | 3 | fmt: 4 | go fmt ./... 5 | 6 | vet: 7 | go vet ./... 8 | 9 | test: 10 | go test ./... -v -coverprofile=profile.cov 11 | 12 | build: fmt vet test 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | maildir 2 | ======= 3 | [![GitHub release](https://img.shields.io/github/release/amalfra/maildir.svg)](https://github.com/amalfra/maildir/releases) 4 | ![Build Status](https://github.com/amalfra/maildir/actions/workflows/test.yml/badge.svg?branch=main) 5 | [![GoDoc](https://godoc.org/github.com/amalfra/maildir/v4?status.svg)](https://godoc.org/github.com/amalfra/maildir/v4) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/amalfra/maildir/v4)](https://goreportcard.com/report/github.com/amalfra/maildir/v4) 7 | [![Coverage Status](https://coveralls.io/repos/github/amalfra/maildir/badge.svg?branch=main)](https://coveralls.io/github/amalfra/maildir?branch=main) 8 | 9 | A go package for reading and writing messages in the maildir format. 10 | 11 | > The Maildir e-mail format is a common way of storing e-mail messages, where each message is kept in a separate file with a unique name, and each folder is a directory. The local filesystem handles file locking as messages are added, moved and deleted. A major design goal of Maildir is to eliminate program code having to handle locking, which is often difficult. 12 | 13 | Refer http://cr.yp.to/proto/maildir.html and http://en.wikipedia.org/wiki/Maildir 14 | 15 | ## Installation 16 | 17 | You can download the package using 18 | 19 | ``` go 20 | go get github.com/amalfra/maildir/v4 21 | ``` 22 | 23 | ## Usage 24 | 25 | Next, import the package 26 | 27 | ``` go 28 | import ( 29 | "github.com/amalfra/maildir/v4" 30 | ) 31 | ``` 32 | 33 | #### Create a maildir in /home/amal/mail 34 | ``` go 35 | myMaildir := maildir.NewMaildir("/home/amal/mail") 36 | ``` 37 | 38 | This command automatically creates the standard Maildir directories - `cur`, 39 | `new`, and `tmp` - if they do not exist. 40 | 41 | #### Add a new message 42 | This creates a new file with the contents "foo"; returns the Message struct reference. Messages are written to the tmp dir then moved to new. 43 | ``` go 44 | message, err := myMaildir.Add("foo") 45 | ``` 46 | 47 | #### List new messages 48 | ``` go 49 | mailList, err := myMaildir.List("new") 50 | ``` 51 | This will return a map of messages by key, sorted by key 52 | 53 | #### List current messages 54 | ``` go 55 | mailList, err := myMaildir.List("cur") 56 | ``` 57 | This will return a map of messages by key, sorted by key 58 | 59 | #### Find the message using key 60 | ``` go 61 | message := maildir.Get(key) 62 | ``` 63 | 64 | #### Delete the message from disk by key 65 | ``` go 66 | err := maildir.Delete(key) 67 | ``` 68 | 69 | **Below are the methods that are available on Message instance** 70 | 71 | #### Get the key used to uniquely identify the message 72 | ``` go 73 | key := message.Key() 74 | ``` 75 | 76 | #### Load the message content from file 77 | ``` go 78 | data, err := message.GetData() 79 | ``` 80 | 81 | #### Process message - move the message from "new" to "cur" 82 | This is usaully done to indicate that some process has retrieved the message. 83 | ``` go 84 | key, err := message.Process("new") 85 | ``` 86 | 87 | ## Development 88 | 89 | Questions, problems or suggestions? Please post them on the [issue tracker](https://github.com/amalfra/maildir/issues). 90 | 91 | You can contribute changes by forking the project and submitting a pull request. You can ensure the tests are passing by running ```make test```. Feel free to contribute :heart_eyes: 92 | 93 | ## UNDER MIT LICENSE 94 | 95 | The MIT License (MIT) 96 | 97 | Copyright (c) 2017 Amal Francis 98 | 99 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 100 | 101 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 102 | 103 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 104 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/amalfra/maildir/v4 2 | 3 | go 1.23.0 4 | -------------------------------------------------------------------------------- /lib/consts.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | // the seperator between unique name and info 4 | const colon = ':' 5 | 6 | // default info, to which flags are appended 7 | const info = "2," 8 | 9 | // Subdirs has subdirectories that are required in maildir 10 | var Subdirs = []string{"tmp", "new", "cur"} 11 | -------------------------------------------------------------------------------- /lib/message.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | // Message represents a maildir message and has it's supported operations 13 | type Message struct { 14 | dir string 15 | maildir string 16 | info string 17 | unqiueName string 18 | oldKey string 19 | } 20 | 21 | // NewMessage will create new message in specified mail directory 22 | func NewMessage(maildir string) (*Message, error) { 23 | var err error 24 | msg := new(Message) 25 | msg.maildir = maildir 26 | msg.dir = "tmp" 27 | msg.unqiueName, err = generate() 28 | if err != nil { 29 | return nil, errors.New("Failed to generate unqiue name") 30 | } 31 | return msg, nil 32 | } 33 | 34 | // parseKey will set dir, unqiueName, info based on the key 35 | func (m *Message) parseKey(key string) { 36 | // remove leading / 37 | key = strings.TrimPrefix(key, string(os.PathSeparator)) 38 | parts := strings.Split(key, string(os.PathSeparator)) 39 | m.dir = parts[0] 40 | filename := parts[1] 41 | parts = strings.Split(filename, string(colon)) 42 | m.unqiueName = parts[0] 43 | if len(parts) > 1 { 44 | m.info = parts[1] 45 | } 46 | } 47 | 48 | // LoadMessage will populate message object by loading info from passed key 49 | func LoadMessage(maildir string, key string) *Message { 50 | msg := new(Message) 51 | msg.maildir = maildir 52 | msg.parseKey(key) 53 | return msg 54 | } 55 | 56 | // filename returns the filename of the message 57 | func (m *Message) filename() string { 58 | return fmt.Sprintf("%s%c%s", m.unqiueName, colon, m.info) 59 | } 60 | 61 | // Key returns the key to identify the message 62 | func (m *Message) Key() string { 63 | return filepath.Join(m.dir, m.filename()) 64 | } 65 | 66 | // path returns the full path to the message 67 | func (m *Message) path() string { 68 | return filepath.Join(m.maildir, m.Key()) 69 | } 70 | 71 | // oldPath returns the old full path to the message 72 | func (m *Message) oldPath() string { 73 | return filepath.Join(m.maildir, m.oldKey) 74 | } 75 | 76 | // rename the message. Returns the new key if successful 77 | func (m *Message) rename(newDir string, newInfo string) (string, error) { 78 | // Save the old key so we can revert to the old state 79 | m.oldKey = m.Key() 80 | 81 | // Set the new state 82 | m.dir = newDir 83 | if newInfo != "" { 84 | m.info = newInfo 85 | } 86 | 87 | if m.oldPath() != m.path() { 88 | err := os.Rename(m.oldPath(), m.path()) 89 | if err != nil { 90 | // restore old state 91 | if m.oldKey != "" { 92 | m.parseKey(m.oldKey) 93 | } 94 | return "", errors.New("Failed to rename folder") 95 | } 96 | } 97 | m.oldKey = "" 98 | return m.Key(), nil 99 | } 100 | 101 | // Write will write data to disk. only work with messages which haven't been written to disk. 102 | // After successfully writing to disk, rename the message to new dir 103 | func (m *Message) Write(data string) error { 104 | if m.dir != "tmp" { 105 | return errors.New("Can only write messages in tmp") 106 | } 107 | 108 | err := ioutil.WriteFile(m.path(), []byte(data), os.ModePerm) 109 | if err != nil { 110 | return fmt.Errorf("Failed to write message to path %s", m.path()) 111 | } 112 | 113 | _, err = m.rename("new", "") 114 | if err != nil { 115 | return errors.New("Failed to rename folder") 116 | } 117 | 118 | return nil 119 | } 120 | 121 | // Process will move a message from new to cur, add info. Returns the message's key 122 | func (m *Message) Process() (string, error) { 123 | return m.rename("cur", info) 124 | } 125 | 126 | // SetInfo will set info on a message 127 | func (m *Message) SetInfo(infoStr string) (string, error) { 128 | if m.dir != "cur" { 129 | return "", errors.New("Can only set info on cur messages") 130 | } 131 | return m.rename("cur", infoStr) 132 | } 133 | 134 | // GetData returns the message's data from disk 135 | func (m *Message) GetData() (string, error) { 136 | dat, err := ioutil.ReadFile(m.path()) 137 | strDat := string(dat) 138 | return strDat, err 139 | } 140 | 141 | // Destroy will remove the message file 142 | func (m *Message) Destroy() error { 143 | return os.Remove(m.path()) 144 | } 145 | -------------------------------------------------------------------------------- /lib/message_test.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "regexp" 8 | "testing" 9 | ) 10 | 11 | var mailDir string 12 | var testData string 13 | var msg *Message 14 | 15 | func cleanMaildir() { 16 | err := os.RemoveAll(mailDir) 17 | if err != nil { 18 | fmt.Fprintln(os.Stderr, "Failed to clean maildir folder") 19 | os.Exit(1) 20 | } 21 | } 22 | 23 | func createMaildir() { 24 | for _, subDir := range Subdirs { 25 | err := os.MkdirAll(filepath.Join(mailDir, subDir), os.ModePerm) 26 | if err != nil { 27 | fmt.Fprintln(os.Stderr, "Failed to create directory structure for maildir folder") 28 | os.Exit(1) 29 | } 30 | } 31 | } 32 | 33 | func init() { 34 | var err error 35 | mailDir = "/tmp/maildir-test" 36 | testData = "foo" 37 | cleanMaildir() 38 | createMaildir() 39 | 40 | msg, err = NewMessage(mailDir) 41 | if err != nil { 42 | fmt.Fprintln(os.Stderr, "Failed to create message") 43 | os.Exit(1) 44 | } 45 | } 46 | 47 | func TestUnwrittenMessageDirShouldBeTemp(t *testing.T) { 48 | matched, _ := regexp.MatchString("(.*)tmp(.*)", msg.path()) 49 | if msg.dir != "tmp" || !matched { 50 | t.Fatalf("unwritten message dir != tmp") 51 | } 52 | } 53 | 54 | func TestUnwrittenMessageHasUniquename(t *testing.T) { 55 | if len(msg.unqiueName) == 0 { 56 | t.Fatalf("unwritten message unqiueName empty") 57 | } 58 | } 59 | 60 | func TestUnwrittenMessageHasFilename(t *testing.T) { 61 | if len(msg.filename()) == 0 { 62 | t.Fatalf("unwritten message filename empty") 63 | } 64 | } 65 | 66 | func TestUnwrittenMessageHasNoInfo(t *testing.T) { 67 | if msg.info != "" { 68 | t.Fatalf("unwritten message info not empty") 69 | } 70 | } 71 | 72 | func TestUnwrittenMessageNotAbleToSetInfo(t *testing.T) { 73 | _, err := msg.SetInfo("test") 74 | if err == nil { 75 | t.Fatalf("unwritten message info shouldn't be updated") 76 | } 77 | } 78 | 79 | func TestCreateWrittenMessage(t *testing.T) { 80 | var err error 81 | cleanMaildir() 82 | createMaildir() 83 | 84 | msg, err = NewMessage(mailDir) 85 | if err != nil { 86 | t.Fatalf("failed to create message") 87 | } 88 | err = msg.Write(testData) 89 | if err != nil { 90 | t.Fatalf(fmt.Sprintf("failed to write to message. got error: %s", err)) 91 | } 92 | } 93 | 94 | func TestWrittenMessageNotWritable(t *testing.T) { 95 | var err error 96 | err = msg.Write("noway!") 97 | if err == nil { 98 | t.Fatalf("it shouldn't be possbile to write to already written message") 99 | } 100 | } 101 | 102 | func TestWrittenMessageHaveNoInfo(t *testing.T) { 103 | if msg.info != "" { 104 | t.Fatalf("info should be empty") 105 | } 106 | } 107 | 108 | func TestWrittenMessageNotAbleToSetInfo(t *testing.T) { 109 | _, err := msg.SetInfo("test") 110 | if err == nil { 111 | t.Fatalf("written message info shouldn't be updated") 112 | } 113 | } 114 | 115 | func TestWrittenMessageDirShouldBeNew(t *testing.T) { 116 | matched, _ := regexp.MatchString("(.*)new(.*)", msg.path()) 117 | if msg.dir != "new" || !matched { 118 | t.Fatalf("unwritten message dir != new") 119 | } 120 | } 121 | 122 | func TestWrittenMessageShouldHaveFile(t *testing.T) { 123 | if _, err := os.Stat(msg.path()); os.IsNotExist(err) { 124 | t.Fatalf("file should exist") 125 | } 126 | } 127 | 128 | func TestWrittenMessageHasCorrectData(t *testing.T) { 129 | data, err := msg.GetData() 130 | if err != nil { 131 | t.Fatalf("failed to read file") 132 | } 133 | if data != testData { 134 | t.Fatalf("incorrect data in file") 135 | } 136 | } 137 | 138 | func TestCreateProcessedMessage(t *testing.T) { 139 | var err error 140 | cleanMaildir() 141 | createMaildir() 142 | 143 | msg, err = NewMessage(mailDir) 144 | if err != nil { 145 | t.Fatalf("failed to create message") 146 | } 147 | err = msg.Write(testData) 148 | if err != nil { 149 | t.Fatalf(fmt.Sprintf("failed to write to message. got error: %s", err)) 150 | } 151 | _, err = msg.Process() 152 | if err != nil { 153 | t.Fatalf(fmt.Sprintf("failed to process message. got error: %s", err)) 154 | } 155 | } 156 | 157 | func TestProcessedMessageNotWritable(t *testing.T) { 158 | var err error 159 | err = msg.Write("noway!") 160 | if err == nil { 161 | t.Fatalf("it shouldn't be possbile to write to already processed message") 162 | } 163 | } 164 | 165 | func TestProcessedMessageDirShouldBeCur(t *testing.T) { 166 | matched, _ := regexp.MatchString("(.*)cur(.*)", msg.path()) 167 | if msg.dir != "cur" || !matched { 168 | t.Fatalf("unwritten message dir != cur") 169 | } 170 | } 171 | 172 | func TestProcessedMessageHaveInfo(t *testing.T) { 173 | if msg.info != info { 174 | t.Fatalf("info not correct") 175 | } 176 | } 177 | 178 | func TestProcessedMessageAbleToSetInfo(t *testing.T) { 179 | _, err := msg.SetInfo("test-info") 180 | if err != nil { 181 | t.Fatalf(fmt.Sprintf("failed to update info. got error: %s", err)) 182 | } 183 | if msg.info != "test-info" { 184 | t.Fatalf("info not correct") 185 | } 186 | matched, _ := regexp.MatchString("(.*)test-info(.*)", msg.path()) 187 | if !matched { 188 | t.Fatalf("correct info not present in message path") 189 | } 190 | } 191 | 192 | func TestProcessedMessageDestroy(t *testing.T) { 193 | err := msg.Destroy() 194 | if err != nil { 195 | t.Fatalf(fmt.Sprintf("failed to destroy message. got error: %s", err)) 196 | } 197 | 198 | if _, err = os.Stat(msg.path()); err == nil { 199 | t.Fatalf(fmt.Sprintf("destroyed file still exists! got error: %s", err)) 200 | } 201 | } 202 | 203 | func TestBadMessagePathErrorForData(t *testing.T) { 204 | cleanMaildir() 205 | createMaildir() 206 | 207 | _, err := msg.GetData() 208 | if err == nil { 209 | t.Fatalf("no error for bad message path") 210 | } 211 | } 212 | 213 | func TestBadMessagePathNotProcessed(t *testing.T) { 214 | _, err := msg.Process() 215 | if err == nil { 216 | t.Fatalf("no error when processing bad message path") 217 | } 218 | } 219 | 220 | func TestBadMessagePathResetMessageKey(t *testing.T) { 221 | oldKey := msg.Key() 222 | msg.Process() 223 | if oldKey != msg.Key() { 224 | t.Fatalf("message key not getting reset") 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /lib/uniqueName.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "time" 8 | ) 9 | 10 | type uniqueName struct { 11 | now time.Time 12 | } 13 | 14 | // generate will create and return unique file names for new messages 15 | func generate() (string, error) { 16 | uni := &uniqueName{now: time.Now()} 17 | hostname, err := uni.right() 18 | if err != nil { 19 | return "", errors.New("Failed to fetch hostname") 20 | } 21 | 22 | return fmt.Sprintf("%d.%s.%s", uni.left(), uni.middle(), hostname), nil 23 | } 24 | 25 | // left part of the unique name is the number of seconds since the UNIX epoch 26 | func (uni *uniqueName) left() int64 { 27 | return uni.now.Unix() 28 | } 29 | 30 | func (uni *uniqueName) microseconds() int64 { 31 | return uni.now.Unix() * 1000000 32 | } 33 | 34 | // middle part of the unique name contains microsecond, process id, and a per-process incrementing counter 35 | func (uni *uniqueName) middle() string { 36 | return fmt.Sprintf("M%06dP%dQ%d", uni.microseconds(), os.Getpid(), getCounter()) 37 | } 38 | 39 | // right part is the hostname 40 | func (uni *uniqueName) right() (string, error) { 41 | name, err := os.Hostname() 42 | if err != nil { 43 | return "", err 44 | } 45 | return name, nil 46 | } 47 | -------------------------------------------------------------------------------- /lib/utils.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | var counter int 8 | var mu sync.Mutex 9 | 10 | // getCounter will return the value of a global per-process incrementing counter 11 | func getCounter() int { 12 | return count() 13 | } 14 | 15 | // resetCounter will reset incrementing counter's value 16 | func resetCounter() { 17 | counter = 0 18 | } 19 | 20 | // count will increment the atomic counter and return its value 21 | func count() int { 22 | mu.Lock() 23 | counter++ 24 | mu.Unlock() 25 | return counter 26 | } 27 | 28 | // StringInSlice will return whether string exists in given slice or not 29 | func StringInSlice(a string, list []string) bool { 30 | for _, b := range list { 31 | if b == a { 32 | return true 33 | } 34 | } 35 | return false 36 | } 37 | -------------------------------------------------------------------------------- /lib/utils_test.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestGetCounter(t *testing.T) { 10 | resetCounter() 11 | for i := 0; i < 100; i++ { 12 | go func() { 13 | for i := 0; i < 10000; i++ { 14 | getCounter() 15 | } 16 | }() 17 | } 18 | 19 | time.Sleep(time.Second) 20 | countVal := getCounter() 21 | 22 | if countVal != 1000001 { 23 | t.Fatalf(fmt.Sprintf("Mutex not working as expected, counter returned %d", countVal)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /maildir.go: -------------------------------------------------------------------------------- 1 | package maildir 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/amalfra/maildir/v4/lib" 11 | ) 12 | 13 | // Maildir implements maildir format and it's operations 14 | type Maildir struct { 15 | path string 16 | } 17 | 18 | // NewMaildir will create new maildir at specified path 19 | func NewMaildir(path string) *Maildir { 20 | maildir := new(Maildir) 21 | maildir.path = path 22 | 23 | _, dCur := os.Stat(path + "/cur") 24 | _, dTmp := os.Stat(path + "/tmp") 25 | _, dNew := os.Stat(path + "/new") 26 | if os.IsNotExist(dCur) || os.IsNotExist(dTmp) || os.IsNotExist(dNew) { 27 | maildir.createDirectories() 28 | } 29 | 30 | return maildir 31 | } 32 | 33 | // createDirectories will the sub directories required by maildir 34 | func (m *Maildir) createDirectories() { 35 | for _, subDir := range lib.Subdirs { 36 | os.MkdirAll(filepath.Join(m.path, subDir), os.ModePerm) 37 | } 38 | } 39 | 40 | // Add writes data out as a new message. Returns Message instance 41 | func (m *Maildir) Add(data string) (*lib.Message, error) { 42 | msg, err := lib.NewMessage(m.path) 43 | if err != nil { 44 | return nil, errors.New("failed to create message") 45 | } 46 | err = msg.Write(data) 47 | if err != nil { 48 | return nil, errors.New("failed to write message") 49 | } 50 | 51 | return msg, nil 52 | } 53 | 54 | // Get returns a message object for key 55 | func (m *Maildir) Get(key string) *lib.Message { 56 | return lib.LoadMessage(m.path, key) 57 | } 58 | 59 | // List returns an array of messages from new or cur directory, sorted by key 60 | func (m *Maildir) List(dir string) (map[string]*lib.Message, error) { 61 | if !lib.StringInSlice(dir, lib.Subdirs) { 62 | return nil, errors.New("dir must be :new, :cur, or :tmp") 63 | } 64 | 65 | keys, err := m.getDirListing(dir) 66 | if err != nil { 67 | return nil, errors.New("failed to get directory listing") 68 | } 69 | sort.Sort(sort.StringSlice(keys)) 70 | 71 | // map keys to message objects 72 | keyMap := make(map[string]*lib.Message) 73 | for _, key := range keys { 74 | keyMap[key] = m.Get(key) 75 | } 76 | 77 | return keyMap, nil 78 | } 79 | 80 | // getDirListing returns an array of keys in dir 81 | func (m *Maildir) getDirListing(dir string) ([]string, error) { 82 | filter := "*" 83 | searchPath := filepath.Join(m.path, dir, filter) 84 | filePaths, err := filepath.Glob(searchPath) 85 | // remove maildir path so that only key remains 86 | for i, filePath := range filePaths { 87 | filePaths[i] = strings.TrimPrefix(filePath, m.path) 88 | } 89 | return filePaths, err 90 | } 91 | 92 | // Delete a message by key 93 | func (m *Maildir) Delete(key string) error { 94 | return m.Get(key).Destroy() 95 | } 96 | -------------------------------------------------------------------------------- /maildir_test.go: -------------------------------------------------------------------------------- 1 | package maildir 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/amalfra/maildir/v4/lib" 10 | ) 11 | 12 | var mailDir string 13 | var testData string 14 | var maildir *Maildir 15 | 16 | func cleanMaildir() { 17 | err := os.RemoveAll(mailDir) 18 | if err != nil { 19 | fmt.Fprintln(os.Stderr, "Failed to clean maildir folder") 20 | os.Exit(1) 21 | } 22 | } 23 | 24 | func init() { 25 | mailDir = "/tmp/maildir-test" 26 | testData = "foo" 27 | cleanMaildir() 28 | 29 | maildir = NewMaildir(mailDir) 30 | } 31 | 32 | func TestPathProperty(t *testing.T) { 33 | if maildir.path != mailDir { 34 | t.Fatalf("path mismatch") 35 | } 36 | } 37 | 38 | func TestDirectoriesCreated(t *testing.T) { 39 | for _, subDir := range lib.Subdirs { 40 | if _, err := os.Stat(filepath.Join(mailDir, subDir)); os.IsNotExist(err) { 41 | t.Fatalf("required sub directories not created") 42 | } 43 | } 44 | } 45 | 46 | func TestAdd(t *testing.T) { 47 | msg, err := maildir.Add(testData) 48 | if err != nil { 49 | t.Fatalf(fmt.Sprintf("failed to add message. got error: %s", err)) 50 | } 51 | msgData, err := msg.GetData() 52 | if err != nil { 53 | t.Fatalf(fmt.Sprintf("failed to get message data. got error: %s", err)) 54 | } 55 | if msgData != testData { 56 | t.Fatalf("incorrect data saved in message") 57 | } 58 | } 59 | 60 | func TestListNewOneMessage(t *testing.T) { 61 | listing, err := maildir.List("new") 62 | if err != nil { 63 | t.Fatalf(fmt.Sprintf("failed to get messages listing. got error: %s", err)) 64 | } 65 | 66 | for _, v := range listing { 67 | fileData, err := v.GetData() 68 | if err != nil { 69 | t.Fatalf(fmt.Sprintf("failed to read messages. got error: %s", err)) 70 | } 71 | if fileData != testData { 72 | t.Fatalf("incorrect data saved in message") 73 | } 74 | } 75 | } 76 | 77 | func TestListNewMultipleMessage(t *testing.T) { 78 | TestAdd(t) 79 | listing, err := maildir.List("new") 80 | if err != nil { 81 | t.Fatalf(fmt.Sprintf("failed to get messages listing. got error: %s", err)) 82 | } 83 | 84 | for _, v := range listing { 85 | fileData, err := v.GetData() 86 | if err != nil { 87 | t.Fatalf(fmt.Sprintf("failed to read messages. got error: %s", err)) 88 | } 89 | if fileData != testData { 90 | t.Fatalf("incorrect data saved in message") 91 | } 92 | } 93 | } 94 | 95 | func TestProcessNewMessages(t *testing.T) { 96 | listing, err := maildir.List("cur") 97 | if err != nil { 98 | t.Fatalf(fmt.Sprintf("failed to get messages listing. got error: %s", err)) 99 | } 100 | 101 | for _, v := range listing { 102 | _, err := v.Process() 103 | if err != nil { 104 | t.Fatalf(fmt.Sprintf("failed to process messages. got error: %s", err)) 105 | } 106 | } 107 | } 108 | 109 | func TestListCurMultipleMessages(t *testing.T) { 110 | listing, err := maildir.List("cur") 111 | if err != nil { 112 | t.Fatalf(fmt.Sprintf("failed to get messages listing. got error: %s", err)) 113 | } 114 | 115 | for _, v := range listing { 116 | fileData, err := v.GetData() 117 | if err != nil { 118 | t.Fatalf(fmt.Sprintf("failed to read messages. got error: %s", err)) 119 | } 120 | if fileData != testData { 121 | t.Fatalf("incorrect data saved in message") 122 | } 123 | } 124 | } 125 | 126 | func TestDeleteMessages(t *testing.T) { 127 | listing, err := maildir.List("cur") 128 | if err != nil { 129 | t.Fatalf(fmt.Sprintf("failed to get messages listing. got error: %s", err)) 130 | } 131 | 132 | for _, v := range listing { 133 | err = maildir.Delete(v.Key()) 134 | if err != nil { 135 | t.Fatalf(fmt.Sprintf("failed to delete messages. got error: %s", err)) 136 | } 137 | } 138 | } 139 | --------------------------------------------------------------------------------