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  }