diff --git a/go.mod b/go.mod index 0affccb..19eedc5 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,9 @@ require ( github.com/emersion/go-imap v1.2.0 github.com/emersion/go-imap-sortthread v1.2.0 github.com/emersion/go-maildir v0.2.0 + github.com/emersion/go-mbox v1.0.2 github.com/emersion/go-message v0.15.0 - github.com/emersion/go-msgauth v0.6.5 // indirect + github.com/emersion/go-msgauth v0.6.5 github.com/emersion/go-pgpmail v0.2.0 github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac github.com/emersion/go-smtp v0.15.0 @@ -26,7 +27,6 @@ require ( github.com/imdario/mergo v0.3.12 github.com/kyoh86/xdg v1.2.0 github.com/lithammer/fuzzysearch v1.1.3 - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.14 github.com/mattn/go-pointer v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.13 diff --git a/go.sum b/go.sum index e6e93f2..b7e770b 100644 --- a/go.sum +++ b/go.sum @@ -45,6 +45,7 @@ github.com/arran4/golang-ical v0.0.0-20220517104411-fd89fefb0182/go.mod h1:BSTTr github.com/brunnre8/go.notmuch v0.0.0-20201126061756-caa2daf7093c h1:dh58QrW3/S/aCnQPFoeRRE9zMauKooDFd5zh1dLtxXs= github.com/brunnre8/go.notmuch v0.0.0-20201126061756-caa2daf7093c/go.mod h1:zJtFvR3NinVdmBiLyB4MyXKmqyVfZEb2cK97ISfTgV8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -68,6 +69,8 @@ github.com/emersion/go-imap-sortthread v1.2.0 h1:EMVEJXPWAhXMWECjR82Rn/tza6Mddcv github.com/emersion/go-imap-sortthread v1.2.0/go.mod h1:UhenCBupR+vSYRnqJkpjSq84INUCsyAK1MLpogv14pE= github.com/emersion/go-maildir v0.2.0 h1:fC4+UVGl8GcQGbFF7AWab2JMf4VbKz+bMNv07xxhzs8= github.com/emersion/go-maildir v0.2.0/go.mod h1:I2j27lND/SRLgxROe50Vam81MSaqPFvJ0OHNnDZ7n84= +github.com/emersion/go-mbox v1.0.2 h1:tE/rT+lEugK9y0myEymCCHnwlZN04hlXPrbKkxRBA5I= +github.com/emersion/go-mbox v1.0.2/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI= github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= github.com/emersion/go-message v0.11.2/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= github.com/emersion/go-message v0.14.1/go.mod h1:N1JWdZQ2WRUalmdHAX308CWBq747VJ8oUorFI3VCBwU= @@ -106,7 +109,9 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.63.2 h1:kwN3umicd2HF3Tgvap4um1ZG52/WyKT9GGdPx0CJk6Y= github.com/go-ini/ini v1.63.2/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= +github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 h1:gBeyun7mySAKWg7Fb0GOcv0upX9bdaZScs8QcRo8mEY= github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -164,20 +169,22 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc= github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= +github.com/jhillyerd/enmime v0.9.1 h1:HcC2WZA6dMCobs8WeyF/6FRSvdRCrr8O+UiLBae4eNE= github.com/jhillyerd/enmime v0.9.1/go.mod h1:S5ge4lnv/dDDBbAWwtoOFlj14NHiXdw/EqMB2lJz3b8= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kyoh86/xdg v1.2.0 h1:CERuT/ShdTDj+A2UaX3hQ3mOV369+Sj+wyn2nIRIIkI= github.com/kyoh86/xdg v1.2.0/go.mod h1:/mg8zwu1+qe76oTFUBnyS7rJzk7LLC0VGEzJyJ19DHs= @@ -202,10 +209,14 @@ github.com/miolini/datacounter v1.0.2 h1:mGTL0vqEAtH7mwNJS1JIpd6jwTAP6cBQQ2P8apa github.com/miolini/datacounter v1.0.2/go.mod h1:C45dc2hBumHjDpEU64IqPwR6TDyPVpzOqqRTN7zmBUA= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -219,10 +230,10 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab h1:ZjX6I48eZSFetPb41dHudEyVr5v953N15TsNZXlkcWY= github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab/go.mod h1:/PfPXh0EntGc3QAAyUaviy4S9tzy4Zp0e2ilq4voC6E= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= @@ -491,11 +502,13 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/worker/lib/search.go b/worker/lib/search.go new file mode 100644 index 0000000..c7d3bee --- /dev/null +++ b/worker/lib/search.go @@ -0,0 +1,254 @@ +package lib + +import ( + "io/ioutil" + "net/textproto" + "strings" + "unicode" + + "git.sr.ht/~sircmpwn/getopt" + + "git.sr.ht/~rjarry/aerc/models" +) + +type searchCriteria struct { + Header textproto.MIMEHeader + Body []string + Text []string + + WithFlags []models.Flag + WithoutFlags []models.Flag +} + +func GetSearchCriteria(args []string) (*searchCriteria, error) { + criteria := &searchCriteria{Header: make(textproto.MIMEHeader)} + + opts, optind, err := getopt.Getopts(args, "rux:X:bat:H:f:c:") + if err != nil { + return nil, err + } + body := false + text := false + for _, opt := range opts { + switch opt.Option { + case 'r': + criteria.WithFlags = append(criteria.WithFlags, models.SeenFlag) + case 'u': + criteria.WithoutFlags = append(criteria.WithoutFlags, models.SeenFlag) + case 'x': + criteria.WithFlags = append(criteria.WithFlags, getParsedFlag(opt.Value)) + case 'X': + criteria.WithoutFlags = append(criteria.WithoutFlags, getParsedFlag(opt.Value)) + case 'H': + // TODO + case 'f': + criteria.Header.Add("From", opt.Value) + case 't': + criteria.Header.Add("To", opt.Value) + case 'c': + criteria.Header.Add("Cc", opt.Value) + case 'b': + body = true + case 'a': + text = true + } + } + if text { + criteria.Text = args[optind:] + } else if body { + criteria.Body = args[optind:] + } else { + for _, arg := range args[optind:] { + criteria.Header.Add("Subject", arg) + } + } + return criteria, nil +} + +func getParsedFlag(name string) models.Flag { + var f models.Flag + switch strings.ToLower(name) { + case "seen": + f = models.SeenFlag + case "answered": + f = models.AnsweredFlag + case "flagged": + f = models.FlaggedFlag + } + return f +} + +func Search(messages []RawMessage, criteria *searchCriteria) ([]uint32, error) { + requiredParts := getRequiredParts(criteria) + + matchedUids := []uint32{} + for _, m := range messages { + success, err := searchMessage(m, criteria, requiredParts) + if err != nil { + return nil, err + } else if success { + matchedUids = append(matchedUids, m.UID()) + } + } + + return matchedUids, nil +} + +// searchMessage executes the search criteria for the given RawMessage, +// returns true if search succeeded +func searchMessage(message RawMessage, criteria *searchCriteria, + parts MsgParts) (bool, error) { + + // setup parts of the message to use in the search + // this is so that we try to minimise reading unnecessary parts + var ( + flags []models.Flag + header *models.MessageInfo + body string + all string + err error + ) + + if parts&FLAGS > 0 { + flags, err = message.ModelFlags() + if err != nil { + return false, err + } + } + if parts&HEADER > 0 { + header, err = MessageInfo(message) + if err != nil { + return false, err + } + } + if parts&BODY > 0 { + // TODO: select body properly; this is just an 'all' clone + reader, err := message.NewReader() + if err != nil { + return false, err + } + defer reader.Close() + bytes, err := ioutil.ReadAll(reader) + if err != nil { + return false, err + } + body = string(bytes) + } + if parts&ALL > 0 { + reader, err := message.NewReader() + if err != nil { + return false, err + } + defer reader.Close() + bytes, err := ioutil.ReadAll(reader) + if err != nil { + return false, err + } + all = string(bytes) + } + + // now search through the criteria + // implicit AND at the moment so fail fast + if criteria.Header != nil { + for k, v := range criteria.Header { + headerValue := header.RFC822Headers.Get(k) + for _, text := range v { + if !containsSmartCase(headerValue, text) { + return false, nil + } + } + } + } + if criteria.Body != nil { + for _, searchTerm := range criteria.Body { + if !containsSmartCase(body, searchTerm) { + return false, nil + } + } + } + if criteria.Text != nil { + for _, searchTerm := range criteria.Text { + if !containsSmartCase(all, searchTerm) { + return false, nil + } + } + } + if criteria.WithFlags != nil { + for _, searchFlag := range criteria.WithFlags { + if !containsFlag(flags, searchFlag) { + return false, nil + } + } + } + if criteria.WithoutFlags != nil { + for _, searchFlag := range criteria.WithoutFlags { + if containsFlag(flags, searchFlag) { + return false, nil + } + } + } + return true, nil +} + +// containsFlag returns true if searchFlag appears in flags +func containsFlag(flags []models.Flag, searchFlag models.Flag) bool { + match := false + for _, flag := range flags { + if searchFlag == flag { + match = true + } + } + return match +} + +// containsSmartCase is a smarter version of strings.Contains for searching. +// Is case-insensitive unless substr contains an upper case character +func containsSmartCase(s string, substr string) bool { + if hasUpper(substr) { + return strings.Contains(s, substr) + } + return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) +} + +func hasUpper(s string) bool { + for _, r := range s { + if unicode.IsUpper(r) { + return true + } + } + return false +} + +// The parts of a message, kind of +type MsgParts int + +const NONE MsgParts = 0 +const ( + FLAGS MsgParts = 1 << iota + HEADER + BODY + ALL +) + +// Returns a bitmask of the parts of the message required to be loaded for the +// given criteria +func getRequiredParts(criteria *searchCriteria) MsgParts { + required := NONE + if len(criteria.Header) > 0 { + required |= HEADER + } + if criteria.Body != nil && len(criteria.Body) > 0 { + required |= BODY + } + if criteria.Text != nil && len(criteria.Text) > 0 { + required |= ALL + } + if criteria.WithFlags != nil && len(criteria.WithFlags) > 0 { + required |= FLAGS + } + if criteria.WithoutFlags != nil && len(criteria.WithoutFlags) > 0 { + required |= FLAGS + } + + return required +} diff --git a/worker/mbox/create.go b/worker/mbox/create.go new file mode 100644 index 0000000..7c4d9f7 --- /dev/null +++ b/worker/mbox/create.go @@ -0,0 +1,60 @@ +package mboxer + +import ( + "io" + "os" + "path/filepath" + "strings" +) + +func createMailboxContainer(path string) (*mailboxContainer, error) { + + file, err := os.Open(path) + if err != nil { + return nil, err + } + + defer file.Close() + + fileInfo, err := file.Stat() + if err != nil { + return nil, err + } + + mbdata := &mailboxContainer{mailboxes: make(map[string]*container)} + + openMboxFile := func(path string, r io.Reader) error { + // read mbox file + messages, err := Read(r) + if err != nil { + return err + } + _, name := filepath.Split(path) + name = strings.TrimSuffix(name, ".mbox") + mbdata.mailboxes[name] = &container{filename: path, messages: messages} + return nil + } + + if fileInfo.IsDir() { + files, err := filepath.Glob(filepath.Join(path, "*.mbox")) + if err != nil { + return nil, err + } + for _, file := range files { + f, err := os.Open(file) + if err != nil { + continue + } + if err := openMboxFile(file, f); err != nil { + return nil, err + } + f.Close() + } + } else { + if err := openMboxFile(path, file); err != nil { + return nil, err + } + } + + return mbdata, nil +} diff --git a/worker/mbox/io.go b/worker/mbox/io.go new file mode 100644 index 0000000..3846916 --- /dev/null +++ b/worker/mbox/io.go @@ -0,0 +1,50 @@ +package mboxer + +import ( + "io" + "io/ioutil" + "time" + + "git.sr.ht/~rjarry/aerc/models" + "git.sr.ht/~rjarry/aerc/worker/lib" + "github.com/emersion/go-mbox" +) + +func Read(r io.Reader) ([]lib.RawMessage, error) { + mbr := mbox.NewReader(r) + uid := uint32(0) + messages := make([]lib.RawMessage, 0) + for { + msg, err := mbr.NextMessage() + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + + content, err := ioutil.ReadAll(msg) + if err != nil { + return nil, err + } + + messages = append(messages, &message{ + uid: uid, flags: []models.Flag{models.SeenFlag}, content: content, + }) + + uid++ + } + return messages, nil +} + +func Write(w io.Writer, reader io.Reader, from string, date time.Time) error { + wc := mbox.NewWriter(w) + mw, err := wc.CreateMessage(from, time.Now()) + if err != nil { + return err + } + _, err = io.Copy(mw, reader) + if err != nil { + return err + } + return wc.Close() +} diff --git a/worker/mbox/models.go b/worker/mbox/models.go new file mode 100644 index 0000000..f97530e --- /dev/null +++ b/worker/mbox/models.go @@ -0,0 +1,203 @@ +package mboxer + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + + "git.sr.ht/~rjarry/aerc/models" + "git.sr.ht/~rjarry/aerc/worker/lib" +) + +type mailboxContainer struct { + mailboxes map[string]*container +} + +func (md *mailboxContainer) Names() []string { + files := make([]string, 0) + for file := range md.mailboxes { + files = append(files, file) + } + return files +} + +func (md *mailboxContainer) Mailbox(f string) (*container, bool) { + mb, ok := md.mailboxes[f] + return mb, ok +} + +func (md *mailboxContainer) Create(file string) *container { + md.mailboxes[file] = &container{filename: file} + return md.mailboxes[file] +} + +func (md *mailboxContainer) Remove(file string) error { + delete(md.mailboxes, file) + return nil +} + +func (md *mailboxContainer) DirectoryInfo(file string) *models.DirectoryInfo { + var exists int + if md, ok := md.Mailbox(file); ok { + exists = len(md.Uids()) + } + return &models.DirectoryInfo{ + Name: file, + Flags: []string{}, + ReadOnly: false, + Exists: exists, + Recent: 0, + Unseen: 0, + AccurateCounts: false, + Caps: &models.Capabilities{ + Sort: true, + Thread: false, + }, + } +} + +func (md *mailboxContainer) Copy(dest, src string, uids []uint32) error { + srcmbox, ok := md.Mailbox(src) + if !ok { + return fmt.Errorf("source %s not found", src) + } + destmbox, ok := md.Mailbox(dest) + if !ok { + return fmt.Errorf("destination %s not found", dest) + } + for _, uidSrc := range srcmbox.Uids() { + found := false + for _, uid := range uids { + if uid == uidSrc { + found = true + break + } + } + if found { + msg, err := srcmbox.Message(uidSrc) + if err != nil { + return fmt.Errorf("could not get message with uid %d from folder %s", uidSrc, src) + } + r, err := msg.NewReader() + if err != nil { + return fmt.Errorf("could not get reader for message with uid %d", uidSrc) + } + flags, err := msg.ModelFlags() + if err != nil { + return fmt.Errorf("could not get flags for message with uid %d", uidSrc) + } + destmbox.Append(r, flags) + } + } + md.mailboxes[dest] = destmbox + return nil +} + +type container struct { + filename string + messages []lib.RawMessage +} + +func (f *container) Uids() []uint32 { + uids := make([]uint32, len(f.messages)) + for i, m := range f.messages { + uids[i] = m.UID() + } + return uids +} + +func (f *container) Message(uid uint32) (lib.RawMessage, error) { + for _, m := range f.messages { + if uid == m.UID() { + return m, nil + } + } + return &message{}, fmt.Errorf("uid [%d] not found", uid) +} + +func (f *container) Delete(uids []uint32) (deleted []uint32) { + newMessages := make([]lib.RawMessage, 0) + for _, m := range f.messages { + del := false + for _, uid := range uids { + if m.UID() == uid { + del = true + break + } + } + if del { + deleted = append(deleted, m.UID()) + } else { + newMessages = append(newMessages, m) + } + } + f.messages = newMessages + return +} + +func (f *container) newUid() (next uint32) { + for _, m := range f.messages { + if uid := m.UID(); uid > next { + next = uid + } + } + next++ + return +} + +func (f *container) Append(r io.Reader, flags []models.Flag) error { + data, err := ioutil.ReadAll(r) + if err != nil { + return err + } + f.messages = append(f.messages, &message{ + uid: f.newUid(), + flags: flags, + content: data, + }) + return nil +} + +// message implements the lib.RawMessage interface +type message struct { + uid uint32 + flags []models.Flag + content []byte +} + +func (m *message) NewReader() (io.ReadCloser, error) { + return ioutil.NopCloser(bytes.NewReader(m.content)), nil +} + +func (m *message) ModelFlags() ([]models.Flag, error) { + return m.flags, nil +} + +func (m *message) Labels() ([]string, error) { + return nil, nil +} + +func (m *message) UID() uint32 { + return m.uid +} + +func (m *message) SetFlag(flag models.Flag, state bool) error { + flagSet := make(map[models.Flag]bool) + flags, err := m.ModelFlags() + if err != nil { + return err + } + for _, f := range flags { + flagSet[f] = true + } + flagSet[flag] = state + newFlags := make([]models.Flag, 0) + for flag, isSet := range flagSet { + if isSet { + newFlags = append(newFlags, flag) + } + } + m.flags = newFlags + return nil +} diff --git a/worker/mbox/worker.go b/worker/mbox/worker.go new file mode 100644 index 0000000..c7f105b --- /dev/null +++ b/worker/mbox/worker.go @@ -0,0 +1,379 @@ +package mboxer + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/url" + "os" + "path/filepath" + "sort" + + "git.sr.ht/~rjarry/aerc/models" + "git.sr.ht/~rjarry/aerc/worker/handlers" + "git.sr.ht/~rjarry/aerc/worker/lib" + "git.sr.ht/~rjarry/aerc/worker/types" + gomessage "github.com/emersion/go-message" +) + +func init() { + handlers.RegisterWorkerFactory("mbox", NewWorker) +} + +var errUnsupported = fmt.Errorf("unsupported command") + +type mboxWorker struct { + data *mailboxContainer + name string + folder *container + worker *types.Worker +} + +func NewWorker(worker *types.Worker) (types.Backend, error) { + return &mboxWorker{ + worker: worker, + }, nil +} + +func (w *mboxWorker) handleMessage(msg types.WorkerMessage) error { + var reterr error // will be returned at the end, needed to support idle + + switch msg := msg.(type) { + + case *types.Unsupported: + // No-op + + case *types.Configure: + u, err := url.Parse(msg.Config.Source) + if err != nil { + reterr = err + break + } + dir := u.Path + if u.Host == "~" { + home, err := os.UserHomeDir() + if err != nil { + reterr = err + break + } + dir = filepath.Join(home, u.Path) + } else { + dir = filepath.Join(u.Host, u.Path) + } + w.data, err = createMailboxContainer(dir) + if err != nil || w.data == nil { + w.data = &mailboxContainer{ + mailboxes: make(map[string]*container), + } + reterr = err + break + } else { + w.worker.Logger.Printf("mbox: configured with mbox file %s", dir) + } + + case *types.Connect, *types.Reconnect, *types.Disconnect: + w.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil) + + case *types.ListDirectories: + dirs := w.data.Names() + sort.Strings(dirs) + for _, name := range dirs { + w.worker.PostMessage(&types.Directory{ + Message: types.RespondTo(msg), + Dir: &models.Directory{ + Name: name, + Attributes: nil, + }, + }, nil) + w.worker.PostMessage(&types.DirectoryInfo{ + Info: w.data.DirectoryInfo(name), + }, nil) + } + w.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil) + + case *types.OpenDirectory: + w.name = msg.Directory + var ok bool + w.folder, ok = w.data.Mailbox(w.name) + if !ok { + w.folder = w.data.Create(w.name) + w.worker.PostMessage(&types.Done{ + Message: types.RespondTo(&types.CreateDirectory{})}, nil) + } + w.worker.PostMessage(&types.DirectoryInfo{ + Info: w.data.DirectoryInfo(msg.Directory), + }, nil) + w.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil) + w.worker.Logger.Printf("mbox: %s opened\n", msg.Directory) + + case *types.FetchDirectoryContents: + var infos []*models.MessageInfo + for _, uid := range w.folder.Uids() { + m, err := w.folder.Message(uid) + if err != nil { + w.worker.Logger.Println("mbox: could not get message", err) + continue + } + info, err := lib.MessageInfo(m) + if err != nil { + w.worker.Logger.Println("mbox: could not get message info", err) + continue + } + infos = append(infos, info) + } + uids, err := lib.Sort(infos, msg.SortCriteria) + if err != nil { + reterr = err + break + } + if len(uids) == 0 { + reterr = fmt.Errorf("mbox: no uids in directory") + break + } + w.worker.PostMessage(&types.DirectoryContents{ + Message: types.RespondTo(msg), + Uids: uids, + }, nil) + w.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil) + + case *types.FetchDirectoryThreaded: + reterr = errUnsupported + + case *types.CreateDirectory: + w.data.Create(msg.Directory) + w.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil) + + case *types.RemoveDirectory: + if err := w.data.Remove(msg.Directory); err != nil { + reterr = err + break + } + w.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil) + + case *types.FetchMessageHeaders: + for _, uid := range msg.Uids { + m, err := w.folder.Message(uid) + if err != nil { + reterr = err + break + } + msgInfo, err := lib.MessageInfo(m) + if err != nil { + reterr = err + break + } else { + w.worker.PostMessage(&types.MessageInfo{ + Message: types.RespondTo(msg), + Info: msgInfo, + }, nil) + } + } + w.worker.PostMessage( + &types.Done{Message: types.RespondTo(msg)}, nil) + + case *types.FetchMessageBodyPart: + m, err := w.folder.Message(msg.Uid) + if err != nil { + w.worker.Logger.Printf("could not get message %d: %v", msg.Uid, err) + reterr = err + break + } + + contentReader, err := m.NewReader() + if err != nil { + reterr = fmt.Errorf("could not get message reader: %v", err) + break + } + + fullMsg, err := gomessage.Read(contentReader) + if err != nil { + reterr = fmt.Errorf("could not read message: %v", err) + break + } + + r, err := lib.FetchEntityPartReader(fullMsg, msg.Part) + if err != nil { + w.worker.Logger.Printf( + "could not get body part reader for message=%d, parts=%#v: %v", + msg.Uid, msg.Part, err) + reterr = err + break + } + + w.worker.PostMessage(&types.MessageBodyPart{ + Message: types.RespondTo(msg), + Part: &models.MessageBodyPart{ + Reader: r, + Uid: msg.Uid, + }, + }, nil) + + case *types.FetchFullMessages: + for _, uid := range msg.Uids { + m, err := w.folder.Message(uid) + if err != nil { + w.worker.Logger.Printf("could not get message for uid %d: %v", uid, err) + continue + } + r, err := m.NewReader() + if err != nil { + w.worker.Logger.Printf("could not get message reader: %v", err) + continue + } + defer r.Close() + b, err := ioutil.ReadAll(r) + if err != nil { + w.worker.Logger.Printf("could not get message reader: %v", err) + continue + } + w.worker.PostMessage(&types.FullMessage{ + Message: types.RespondTo(msg), + Content: &models.FullMessage{ + Uid: uid, + Reader: bytes.NewReader(b), + }, + }, nil) + } + w.worker.PostMessage(&types.Done{ + Message: types.RespondTo(msg), + }, nil) + + case *types.DeleteMessages: + deleted := w.folder.Delete(msg.Uids) + if len(deleted) > 0 { + w.worker.PostMessage(&types.MessagesDeleted{ + Message: types.RespondTo(msg), + Uids: deleted, + }, nil) + } + + w.worker.PostMessage(&types.DirectoryInfo{ + Info: w.data.DirectoryInfo(w.name), + }, nil) + + w.worker.PostMessage( + &types.Done{Message: types.RespondTo(msg)}, nil) + + case *types.FlagMessages: + for _, uid := range msg.Uids { + m, err := w.folder.Message(uid) + if err != nil { + w.worker.Logger.Printf("could not get message: %v", err) + continue + } + if err := m.(*message).SetFlag(msg.Flag, msg.Enable); err != nil { + w.worker.Logger.Printf("could change flag %v to %v on message: %v", msg.Flag, msg.Enable, err) + continue + } + info, err := lib.MessageInfo(m) + if err != nil { + w.worker.Logger.Printf("could not get message info: %v", err) + continue + } + + w.worker.PostMessage(&types.MessageInfo{ + Message: types.RespondTo(msg), + Info: info, + }, nil) + } + + w.worker.PostMessage(&types.DirectoryInfo{ + Info: w.data.DirectoryInfo(w.name), + }, nil) + + w.worker.PostMessage( + &types.Done{Message: types.RespondTo(msg)}, nil) + + case *types.CopyMessages: + err := w.data.Copy(msg.Destination, w.name, msg.Uids) + if err != nil { + reterr = err + break + } + + w.worker.PostMessage(&types.DirectoryInfo{ + Info: w.data.DirectoryInfo(w.name), + }, nil) + + w.worker.PostMessage(&types.DirectoryInfo{ + Info: w.data.DirectoryInfo(msg.Destination), + }, nil) + + w.worker.PostMessage( + &types.Done{Message: types.RespondTo(msg)}, nil) + + case *types.SearchDirectory: + criteria, err := lib.GetSearchCriteria(msg.Argv) + if err != nil { + reterr = err + break + } + w.worker.Logger.Printf("Searching with parsed criteria: %#v", criteria) + m := make([]lib.RawMessage, 0, len(w.folder.Uids())) + for _, uid := range w.folder.Uids() { + msg, err := w.folder.Message(uid) + if err != nil { + w.worker.Logger.Println("faild to get message for uid:", uid) + continue + } + m = append(m, msg) + } + uids, err := lib.Search(m, criteria) + if err != nil { + reterr = err + break + } + w.worker.PostMessage(&types.SearchResults{ + Message: types.RespondTo(msg), + Uids: uids, + }, nil) + + case *types.AppendMessage: + if msg.Destination == "" { + reterr = fmt.Errorf("AppendMessage with empty destination directory") + break + } + folder, ok := w.data.Mailbox(msg.Destination) + if !ok { + folder = w.data.Create(msg.Destination) + w.worker.PostMessage(&types.Done{ + Message: types.RespondTo(&types.CreateDirectory{})}, nil) + } + + if err := folder.Append(msg.Reader, msg.Flags); err != nil { + reterr = err + break + } else { + w.worker.PostMessage(&types.DirectoryInfo{ + Info: w.data.DirectoryInfo(msg.Destination), + }, nil) + w.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil) + } + + case *types.AnsweredMessages: + reterr = errUnsupported + default: + reterr = errUnsupported + } + + return reterr +} + +func (w *mboxWorker) Run() { + for { + select { + case msg := <-w.worker.Actions: + msg = w.worker.ProcessAction(msg) + if err := w.handleMessage(msg); err == errUnsupported { + w.worker.PostMessage(&types.Unsupported{ + Message: types.RespondTo(msg), + }, nil) + } else if err != nil { + w.worker.PostMessage(&types.Error{ + Message: types.RespondTo(msg), + Error: err, + }, nil) + } + } + } +} diff --git a/worker/worker_enabled.go b/worker/worker_enabled.go index f0b9dbc..a644525 100644 --- a/worker/worker_enabled.go +++ b/worker/worker_enabled.go @@ -1,5 +1,9 @@ package worker // the following workers are always enabled -import _ "git.sr.ht/~rjarry/aerc/worker/imap" -import _ "git.sr.ht/~rjarry/aerc/worker/maildir" +import ( + _ "git.sr.ht/~rjarry/aerc/worker/imap" + _ "git.sr.ht/~rjarry/aerc/worker/maildir" + + _ "git.sr.ht/~rjarry/aerc/worker/mbox" +)