github.com/la5nta/wl2k-go@v0.11.8/mailbox/syncdir.go (about) 1 // Copyright 2015 Martin Hebnes Pedersen (LA5NTA). All rights reserved. 2 // Use of this source code is governed by the MIT-license that can be 3 // found in the LICENSE file. 4 5 // Package mailbox provides mailbox handlers for a fbb.Session. 6 package mailbox 7 8 import ( 9 "fmt" 10 "io/ioutil" 11 "log" 12 "os" 13 "os/user" 14 "path" 15 "path/filepath" 16 "strings" 17 18 "github.com/la5nta/wl2k-go/fbb" 19 ) 20 21 const ( 22 DIR_INBOX = "/in/" 23 DIR_OUTBOX = "/out/" 24 DIR_SENT = "/sent/" 25 DIR_ARCHIVE = "/archive/" 26 ) 27 28 const Ext = ".b2f" 29 30 // NewDirHandler is a file system (directory) oriented mailbox handler. 31 type DirHandler struct { 32 MBoxPath string 33 deferred map[string]bool 34 sendOnly bool 35 } 36 37 // NewDirHandler wraps the directory given by path as a DirHandler. 38 // 39 // If sendOnly is true, all inbound messages will be deferred. 40 func NewDirHandler(path string, sendOnly bool) *DirHandler { 41 return &DirHandler{ 42 MBoxPath: path, 43 sendOnly: sendOnly, 44 } 45 } 46 47 func (h *DirHandler) Prepare() (err error) { 48 h.deferred = make(map[string]bool) 49 return ensureDirStructure(h.MBoxPath) 50 } 51 52 func (h *DirHandler) Inbox() ([]*fbb.Message, error) { 53 return LoadMessageDir(path.Join(h.MBoxPath, DIR_INBOX)) 54 } 55 56 func (h *DirHandler) Outbox() ([]*fbb.Message, error) { 57 return LoadMessageDir(path.Join(h.MBoxPath, DIR_OUTBOX)) 58 } 59 60 func (h *DirHandler) Sent() ([]*fbb.Message, error) { 61 return LoadMessageDir(path.Join(h.MBoxPath, DIR_SENT)) 62 } 63 64 func (h *DirHandler) Archive() ([]*fbb.Message, error) { 65 return LoadMessageDir(path.Join(h.MBoxPath, DIR_ARCHIVE)) 66 } 67 68 // InboxCount returns the number of messages in the inbox. -1 on error. 69 func (h *DirHandler) InboxCount() int { return countFiles(path.Join(h.MBoxPath, DIR_INBOX)) } 70 func (h *DirHandler) OutboxCount() int { return countFiles(path.Join(h.MBoxPath, DIR_OUTBOX)) } 71 func (h *DirHandler) SentCount() int { return countFiles(path.Join(h.MBoxPath, DIR_SENT)) } 72 func (h *DirHandler) ArchiveCount() int { return countFiles(path.Join(h.MBoxPath, DIR_ARCHIVE)) } 73 74 func (h *DirHandler) AddOut(msg *fbb.Message) error { 75 data, err := msg.Bytes() 76 if err != nil { 77 return err 78 } 79 80 return ioutil.WriteFile(path.Join(h.MBoxPath, DIR_OUTBOX, msg.MID()+Ext), data, 0644) 81 } 82 83 func (h *DirHandler) ProcessInbound(msgs ...*fbb.Message) (err error) { 84 dir := path.Join(h.MBoxPath, DIR_INBOX) 85 for _, m := range msgs { 86 filename := path.Join(dir, m.MID()+Ext) 87 88 m.Header.Set("X-Unread", "true") 89 90 data, err := m.Bytes() 91 if err != nil { 92 return err 93 } 94 95 if err = ioutil.WriteFile(filename, data, 0664); err != nil { 96 return fmt.Errorf("Unable to write received message (%s): %s", filename, err) 97 } 98 } 99 return 100 } 101 102 func (h *DirHandler) GetInboundAnswer(p fbb.Proposal) fbb.ProposalAnswer { 103 if h.sendOnly { 104 return fbb.Defer 105 } 106 107 // Check if file exists 108 f, err := os.Open(path.Join(h.MBoxPath, DIR_INBOX, p.MID()+Ext)) 109 if err == nil { 110 f.Close() 111 return fbb.Reject 112 } else if os.IsNotExist(err) { 113 return fbb.Accept 114 } else if err != nil { 115 log.Printf("Unable to determin if %s has been received: %s", p.MID(), err) 116 } 117 118 return fbb.Accept 119 } 120 121 func (h *DirHandler) SetSent(MID string, rejected bool) { 122 oldPath := path.Join(h.MBoxPath, DIR_OUTBOX, MID+Ext) 123 newPath := path.Join(h.MBoxPath, DIR_SENT, MID+Ext) 124 125 if err := os.Rename(oldPath, newPath); err != nil { 126 log.Fatalf("Unable to move %s to %s: %s", oldPath, newPath, err) 127 } 128 } 129 130 func (h *DirHandler) SetDeferred(MID string) { 131 h.deferred[MID] = true 132 } 133 134 func (h *DirHandler) GetOutbound(fws ...fbb.Address) []*fbb.Message { 135 all, err := LoadMessageDir(path.Join(h.MBoxPath, DIR_OUTBOX)) 136 if err != nil { 137 log.Println(err) 138 } 139 140 deliver := make([]*fbb.Message, 0, len(all)) 141 for _, m := range all { 142 if h.deferred[m.MID()] { 143 continue 144 } 145 146 // Check unsent messages that are addressed to one of the 147 // forwarder addresses of the remote. 148 if len(fws) > 0 { 149 for _, fw := range fws { 150 if m.IsOnlyReceiver(fw) { 151 deliver = append(deliver, m) 152 break 153 } 154 } 155 continue 156 } 157 158 if len(fws) == 0 && m.Header.Get("X-P2POnly") == "true" { 159 continue // The message is P2POnly and remote is CMS 160 } 161 162 // Remove private headers 163 m.Header.Del("X-P2POnly") 164 m.Header.Del("X-FilePath") 165 m.Header.Del("X-Unread") 166 167 deliver = append(deliver, m) 168 } 169 return deliver 170 } 171 172 // Deprecated: implementers should choose their own directories 173 func DefaultMailboxPath() (string, error) { 174 appdir, err := DefaultAppDir() 175 if err != nil { 176 return "", fmt.Errorf("Unable to determine application directory: %s", err) 177 } 178 return path.Join(appdir, "mailbox"), nil 179 } 180 181 // Deprecated: implementers should choose their own directories 182 func DefaultAppDir() (string, error) { 183 usr, err := user.Current() 184 if err != nil { 185 return "", fmt.Errorf("Unable to determine home directory: %s", err) 186 } 187 return path.Join(usr.HomeDir, ".wl2k"), nil 188 } 189 190 func ensureDirStructure(mboxPath string) (err error) { 191 mode := os.ModeDir | os.ModePerm 192 if err = os.MkdirAll(path.Join(mboxPath, DIR_INBOX), mode); err != nil { 193 return 194 } else if err = os.MkdirAll(path.Join(mboxPath, DIR_OUTBOX), mode); err != nil { 195 return 196 } else if err = os.MkdirAll(path.Join(mboxPath, DIR_SENT), mode); err != nil { 197 return 198 } else if err = os.MkdirAll(path.Join(mboxPath, DIR_ARCHIVE), mode); err != nil { 199 return 200 } 201 return 202 } 203 204 func UserPath(root, callsign string) string { 205 return path.Join(root, callsign) 206 } 207 208 func countFiles(dirPath string) int { 209 files, err := ioutil.ReadDir(dirPath) 210 if err != nil { 211 return -1 212 } 213 214 return len(files) 215 } 216 217 func LoadMessageDir(dirPath string) ([]*fbb.Message, error) { 218 files, err := ioutil.ReadDir(dirPath) 219 if err != nil { 220 return nil, fmt.Errorf("Unable to read dir (%s): %s", dirPath, err) 221 } 222 223 msgs := make([]*fbb.Message, 0, len(files)) 224 225 for _, file := range files { 226 if file.IsDir() || file.Name()[0] == '.' { 227 continue 228 } 229 230 if !strings.EqualFold(filepath.Ext(file.Name()), Ext) { 231 continue 232 } 233 234 msg, err := OpenMessage(path.Join(dirPath, file.Name())) 235 if err != nil { 236 return nil, err 237 } 238 239 msgs = append(msgs, msg) 240 } 241 return msgs, nil 242 } 243 244 // OpenMessage opens a single a fbb.Message file. 245 func OpenMessage(path string) (*fbb.Message, error) { 246 f, err := os.Open(path) 247 if err != nil { 248 return nil, fmt.Errorf("Unable to open file (%s): %s", path, err) 249 } 250 defer f.Close() 251 252 message := new(fbb.Message) 253 if err := message.ReadFrom(f); err != nil { 254 f.Close() 255 return nil, fmt.Errorf("Unable to parse message (%s): %s", path, err) 256 } 257 258 message.Header.Set("X-FilePath", path) 259 return message, nil 260 } 261 262 // IsUnread returns true if the given message is marked as unread. 263 func IsUnread(msg *fbb.Message) bool { return msg.Header.Get("X-Unread") == "true" } 264 265 // SetUnread marks the given message as read/unread and re-writes the file to disk. 266 func SetUnread(msg *fbb.Message, unread bool) error { 267 if !unread && msg.Header.Get("X-Unread") == "" { 268 return nil 269 } 270 271 if unread { 272 msg.Header.Set("X-Unread", "true") 273 } else { 274 msg.Header.Del("X-Unread") 275 } 276 277 data, err := msg.Bytes() 278 if err != nil { 279 return err 280 } 281 282 filePath := msg.Header.Get("X-FilePath") 283 if filePath == "" { 284 return fmt.Errorf("Missing X-FilePath header") 285 } 286 return ioutil.WriteFile(filePath, data, 0644) 287 }