├── .gitignore ├── LICENSE ├── README.md ├── conf ├── conf.json ├── private.key └── public.pem ├── db.go ├── docs ├── home_screenshoot.png └── pop-up_screenshoot.png ├── levenshtein.go ├── main.go ├── static ├── apple-touch-icon-114x114.png ├── apple-touch-icon-120x120.png ├── apple-touch-icon-57x57.png ├── apple-touch-icon-60x60.png ├── apple-touch-icon-72x72.png ├── apple-touch-icon-76x76.png ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-196x196.png ├── favicon-32x32.png ├── favicon-96x96.png ├── favicon.ico ├── logo.png ├── mstile-150x150.png ├── mstile-310x150.png └── mstile-70x70.png ├── tmpl └── home.html ├── webmail.go └── webui.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | data/* 4 | logs/* 5 | 6 | *.exe 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GoGuerrilla SMTPd 2 | An minimalist, event-driven I/O, non-blocking SMTP server in Go 3 | 4 | Copyright (c) 2012 Flashmob, GuerrillaMail.com 5 | 6 | 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 7 | permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | 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. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is Theary ? 2 | 3 | Theary is a fake SMTP server that temporarily stores received e-mails. It offers a minimalist webmail to display received e-mails. Theary was designed to offer a volatile SMTP server for demo or load test purposes. It was inspired by smtp4dev, but with web and volume in mind. 4 | 5 | Received e-mails are deleted on a regular basis (depending on ```RECIPIENTS_LIFETIME``` value). 6 | 7 | ![Minimalist webmail client](/docs/home_screenshoot.png "Minimalist webmail client") 8 | 9 | See it in action on http://mail.leave-management-system.org/ in conjunction with http://demo.leave-management-system.org/ 10 | 11 | # Features not covered 12 | 13 | * Theary doesn't relay e-mails to any hytpothetical final recipient. 14 | * Theary uses a nosql db to temporarily store received e-mails, but is not designed to permantly store e-mails. 15 | * Theary implements no security features (all is in clear, etc.). 16 | 17 | # Usage 18 | 19 | * ```theary run``` run the executable from command line 20 | * ```theary install``` install the service 21 | * ```theary remove``` remove the service 22 | * ```theary start``` start the service 23 | * ```theary stop``` stop the service 24 | 25 | # Configuration 26 | 27 | Edit conf/conf.json : 28 | 29 | * ```GM_ALLOWED_HOSTS``` Allowed hosts (comma separated) or any host (```*```). 30 | * ```GSMTP_HOST_NAME``` Fake SMTP hostname 31 | * ```GSMTP_MAX_SIZE``` Max size of e-mails 32 | * ```GSMTP_TIMEOUT``` SMTP timeout 33 | * ```GSMTP_VERBOSE``` Level of log 34 | * ```GSTMP_LISTEN_INTERFACE``` host:port to be listened by the SMTP listener. 35 | * ```GM_MAX_CLIENTS``` Maximun of clients served by the SMTP listener. 36 | * ```WEBUI_MODE``` Mode of the web user interface (embedded web server or served by fastCGI) : 37 | 1. LOCAL : Run as a local web server. 38 | 2. TCP : FCGI via TCP. 39 | 3. UNIX : FCGI via UNIX socket. 40 | * ```WEBUI_SERVE``` host:port to be listened by the web user interface (minimalist webmail client). 41 | * ```RECIPIENTS_LIFETIME``` lifetime (in seconds) of a recipient. If older, will be deleted by the cleaner. 42 | * ```CLEANER_INTERVAL``` duration (in seconds) between two calls to the function that cleans the database. 43 | 44 | # Build 45 | 46 | ```$ go get code.google.com/p/go.exp/fsnotify``` 47 | ```$ go get bitbucket.org/kardianos/service``` 48 | ```$ go get bitbucket.org/kardianos/osext``` 49 | ```$ go get github.com/HouzuoGuo/tiedot/db``` 50 | ```$ go get github.com/gorilla/mux``` 51 | ```$ go get github.com/sloonz/go-qprintable``` 52 | ```$ go build .``` 53 | 54 | # Setup with nginx 55 | 56 | Provided you've launched theary as a FastCGI listener on TCP port 8000, below is an example of the nginx configuration file : 57 | ``` 58 | server { 59 | listen 80; 60 | server_name mail.leave-management-system.org; 61 | access_log /var/log/nginx/mail-lms.access.log combined; 62 | location / { 63 | include fastcgi_params; 64 | fastcgi_pass 127.0.0.1:8000; 65 | } 66 | } 67 | ``` 68 | 69 | # Status 70 | 71 | Theary is under development. 72 | 73 | # Licence 74 | 75 | Release under GPL v3 76 | 77 | # Supported environnements 78 | 79 | Theary is written in pure go and doesn't depend on 3rd party C bindings, so it can run on any environnement supported by go (Windows, Linux, etc.) 80 | 81 | # Credits 82 | 83 | Theary is derivated from https://github.com/shirkey/go-guerrilla project, but has a different purpose. Instead of persisting received mail into a MySQL database, it temporarily strores them in a nosql db in order to display them on a lightweight embedded web ui. Theary doesn't interface with Nginx through a proxy interface but with any webserver supporting FastCGI. 84 | 85 | The typeahead algo is derived from https://github.com/jamra/gocleo but with an optimized code. 86 | 87 | Icon by Maja Bencic - http://www.fritula.hr under Creative Commons (Attribution 3.0 Croatia) 88 | 89 | Theary would not exist without these open source libraries : 90 | * github.com/HouzuoGuo/tiedot 91 | * bitbucket.org/kardianos/osext 92 | * bitbucket.org/kardianos/service 93 | * github.com/gorilla 94 | * golang amazing std lib 95 | -------------------------------------------------------------------------------- /conf/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "GM_ALLOWED_HOSTS":"*", 3 | "GSMTP_HOST_NAME":"mail.example.com", 4 | "GSMTP_MAX_SIZE":"131072", 5 | "GSMTP_TIMEOUT":"100", 6 | "GSMTP_VERBOSE":"Y", 7 | "GSTMP_LISTEN_INTERFACE":"127.0.0.1:25", 8 | "GM_MAX_CLIENTS":"500", 9 | "SGID":"508", 10 | "GUID":"504", 11 | "WEBUI_MODE":"LOCAL", 12 | "WEBUI_SERVE":"127.0.0.1:8000", 13 | "RECIPIENTS_LIFETIME":"60000", 14 | "CLEANER_INTERVAL":"10" 15 | } 16 | -------------------------------------------------------------------------------- /conf/private.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDO9orHcVjXjBaE 3 | CdbotR8M1OSBPg25ismF4mrbypfyw1dO461bju2nrUUSGk0KmNphuHiHljxc3a1h 4 | ZXF6XIBv6WCOy5pcukJiy/d6hJ5Pp7JNy8FmIECQTWCs0zFlI+AgsLh7y/Z/9sHU 5 | q/pr2RLSnC0b1PXwrLVAtAvKDs/rcrwAjpxzUW5lb2O6atayMHqUeJPtc/QucFHM 6 | /e/mD/DBirOvYORiqguLtekPiF1nGUloFDf7Eovgn6dgCMGYc8lh8ovVt6G5NRnV 7 | 19FbS5ZRp3oWu/I5VT10mwwGBOha2JgOBnLrb0W1LjmlEQVaOOO8GLaYxJb7+TKj 8 | lR5dAy+DAgMBAAECggEAFeQWAcqHbyUuLIEt7idaRW0vTkxV/04iyrqMOvtO3yqy 9 | TXHfoFOZ56Z4K9YEWB6a2sM8XHsTn28DK84jFqI6I5w3zmLSzQQWiYSxhJAKfrpq 10 | LbhjmH3I1nqYwy8DhoMo7cxDdzS3uglLr0cRkd7AEu7aBpub9/0Mnu9sX875RxoG 11 | 68AMryrR835JCnKkWwtZ7/Rhvh2dcOPSVhE+sl/ZlHGi+lFypHUbvKdOas+YdtDq 12 | NjxRd9fauGx+Up6sWFDNU3dcQEOHxjyYrMA6RCePmw9koGpXhNNdXW0XVCy/ERwe 13 | wr81vJ43BpV/5d3nH1qZMfBOM181WEa5Z54sHSJqQQKBgQDzWwTIubhsgpiBCkdA 14 | zmSGiknCNQSj/u2PwIX2+lG69SBwxZLDmvOpBFWw74GBLCEZceJ779G2T1Ul7EK3 15 | +nLZ9rTIM/nDZPr6f/6StzVu7Qzg01CMmWkoXHETOujVuSuWGny31jIpcLOdGll5 16 | Juw/zJea3z6WIMOCT8eJ9MI0CQKBgQDZt3Nbgl5SKSVOM59FnX4RdjqS/qA5o7ho 17 | wXEmIzEjeGQUFmawJYY4gtTz8poqI0iCa9WRWMH+xXcp4vhy1OnUhhng5PZxsGQd 18 | g1AyN+My91PaHqzW9QzxdujyZQgoksk6IIUiMsjeDC2OrfSq4UA2KMO5pmpaZOIE 19 | S0A4jPFiKwKBgQC3gVwDLKDYGkZ7j8+ZG8mL9n2WF7qvG43yB2A9lBbLXwqeXy3D 20 | mHXCbsVbTc+fIzK2aD077eR6kCoKFbVd3Fp859781MyzPdNPz/KcmdCOG+zJIC+u 21 | RgSY7dRKhvKKLz6hyslfKwLaYuTeQ79SbzpZBaMQouUj+gLToes6qTlEWQKBgQDU 22 | UKGPoPAogXWe8JqnAfJaRwjSJrvL2gxRJCwavAEEjThTmpDjwIRHAdd4WqLa6vOg 23 | NFbeWkc9FAakc3JEUbwChBAikEaBEjpfyZngjz3iiu1b7cQyEGFh7Ms47yvonVTd 24 | ea87bXkTiZ634I6UQfwjlNdiaZaXtn/vHg9v1orjZwKBgQDRGNsq/GL9SuQyijU1 25 | +1Ukt5F6kEJtdvqZPkx6ArzyKj7oGye+EIm700iLTWCpuyJ2vYeypjnW2ruWcBzf 26 | Anna9uZI4Z7/ImT8jPFJacOW3XY/2MiTkh0NyxHcSdv7JUMjSv6gU6fT98UzzyT8 27 | hUb5YNRv5WRgSISt6reYSJc8Sg== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /conf/public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICnjCCAYYCCQCwliTt/pNGNTANBgkqhkiG9w0BAQUFADARMQ8wDQYDVQQDEwZ1 3 | YnVudHUwHhcNMTExMjExMDMwNjU2WhcNMjExMjA4MDMwNjU2WjARMQ8wDQYDVQQD 4 | EwZ1YnVudHUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDO9orHcVjX 5 | jBaECdbotR8M1OSBPg25ismF4mrbypfyw1dO461bju2nrUUSGk0KmNphuHiHljxc 6 | 3a1hZXF6XIBv6WCOy5pcukJiy/d6hJ5Pp7JNy8FmIECQTWCs0zFlI+AgsLh7y/Z/ 7 | 9sHUq/pr2RLSnC0b1PXwrLVAtAvKDs/rcrwAjpxzUW5lb2O6atayMHqUeJPtc/Qu 8 | cFHM/e/mD/DBirOvYORiqguLtekPiF1nGUloFDf7Eovgn6dgCMGYc8lh8ovVt6G5 9 | NRnV19FbS5ZRp3oWu/I5VT10mwwGBOha2JgOBnLrb0W1LjmlEQVaOOO8GLaYxJb7 10 | +TKjlR5dAy+DAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAMsZUwC3x5dLMcf6KemJ 11 | XOnsTBbkWWrz53R0DHf4B37I0fMeiVuCQ6jlMjF/VDzqPPq1YhgaKpzsgAWAX48n 12 | 75Gj085ZQkBp9wmZOpPYzNs7V8X3ripB9g0di9OE3SkMPpMxrlZprl0crAORljvF 13 | E5IpKJT6A6rTXTEYyvuR/Vx3oq7L4k3rn+RQVchSuE+tVCtm9qlf957Pkwa/hgjB 14 | BaWNU6V3sMmhGznrSjFukRNDoriLAKwByWwq/2MpeUOBQvYM0pK2OqSkpBBdR/9+ 15 | ygYkDT0Ue1u4XU/JE7NOrjEy+9s/aMMEPh9mw66dX5U/EZ1etSecls0B7LQblUgy 16 | ThY= 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | * This file is part of theary. 5 | * 6 | * theary is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * theary is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | * 19 | */ 20 | 21 | import ( 22 | "time" 23 | "io/ioutil" 24 | "strconv" 25 | "log" 26 | ) 27 | 28 | // createIfNotIndB checks if a collection exists or not 29 | // it will create the collection if it doen't exist 30 | func createIfNotIndB(collectionName string ) { 31 | if !existsIndB(collectionName) { 32 | if err := dbEmails.Create(collectionName, 1); err != nil { 33 | panic(err) 34 | } 35 | } 36 | } 37 | 38 | // existsIndB checks if a collection exists or not 39 | func existsIndB(collectionName string ) bool { 40 | found := false 41 | for name := range dbEmails.StrCol { 42 | if name == collectionName { 43 | found = true 44 | break 45 | } 46 | } 47 | return found 48 | } 49 | 50 | // cleaner is regularly triggered to delete old mails and recipients from database 51 | func cleaner(interval *time.Ticker) { 52 | var refTime time.Time 53 | for _ = range interval.C { 54 | duration, _ := strconv.ParseInt(gConfig["RECIPIENTS_LIFETIME"], 10, 64) 55 | duration = duration * -1 56 | refTime = time.Now().Add(time.Duration(duration) * time.Second) 57 | files, _ := ioutil.ReadDir(dataPath) 58 | for _, f := range files { 59 | if f.IsDir() && f.Name() != "recipients" { 60 | if f.ModTime().Before(refTime) { 61 | dbEmails.Drop(f.Name()) 62 | log.Println("Recipient dropped as it exceeded its lifetime", f.Name()) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /docs/home_screenshoot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/docs/home_screenshoot.png -------------------------------------------------------------------------------- /docs/pop-up_screenshoot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/docs/pop-up_screenshoot.png -------------------------------------------------------------------------------- /levenshtein.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | * This file is part of theary. 5 | * 6 | * It uses portion of code from gocleo 7 | * Copyright (c) 2011 jamra.source@gmail.com 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * 10 | * theary is free software: you can redistribute it and/or modify 11 | * it under the terms of the GNU General Public License as published by 12 | * the Free Software Foundation, either version 3 of the License, or 13 | * (at your option) any later version. 14 | * 15 | * theary is distributed in the hope that it will be useful, 16 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | * GNU General Public License for more details. 19 | * 20 | * You should have received a copy of the GNU General Public License 21 | * along with Foobar. If not, see . 22 | * 23 | */ 24 | 25 | import ( 26 | "fmt" 27 | "net/http" 28 | "sort" 29 | "io/ioutil" 30 | "strings" 31 | "encoding/json" 32 | "code.google.com/p/go.exp/fsnotify" 33 | "github.com/gorilla/mux" 34 | ) 35 | 36 | var watcher fsnotify.Watcher 37 | 38 | // watchFolderRecipients watch if there is any modification in the list of recipient and update 39 | // the index of suggestions (for autocompletion purposes in the web ui). 40 | func watchFolderRecipients() { 41 | watcher, err := fsnotify.NewWatcher() 42 | if err != nil { 43 | logFatal("(fsConfigWatcher) fsnotify.NewWatcher() : ", err) 44 | } 45 | 46 | go func() { 47 | for { 48 | select { 49 | case ev := <-watcher.Event: 50 | logInfo("event: %v", ev) 51 | BuildIndexes(nil) 52 | case err := <-watcher.Error: 53 | logInfo("error: %v", err) 54 | } 55 | } 56 | }() 57 | 58 | err = watcher.WatchFlags(dataPath, fsnotify.FSN_MODIFY | fsnotify.FSN_CREATE | fsnotify.FSN_DELETE) 59 | if err != nil { 60 | logln(2, fmt.Sprintf("(fsConfigWatcher) watcher.WatchFlags(fsnotify.FSN_MODIFY) : %s", err)) 61 | } 62 | 63 | } 64 | 65 | //Search handles the web requests and writes the output as 66 | //json data. 67 | func searchHandler(w http.ResponseWriter, r *http.Request) { 68 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 69 | vars := mux.Vars(r) 70 | query := vars["query"] 71 | searchResult := CleoSearch(m.iIndex, m.fIndex, query) 72 | sort.Sort(ByScore{searchResult}) 73 | myJson, _ := json.Marshal(searchResult) 74 | fmt.Fprintf(w, string(myJson)) 75 | } 76 | 77 | func BuildIndexes(scoringFunction fn_score) { 78 | m = &indexContainer{} 79 | m.iIndex = NewInvertedIndex() 80 | m.fIndex = NewForwardIndex() 81 | 82 | chosenScoringFunction = scoringFunction 83 | if scoringFunction == nil { 84 | chosenScoringFunction = Score 85 | } 86 | 87 | //Read folder content 88 | docID := 1 89 | files, _ := ioutil.ReadDir(dataPath) 90 | for _, f := range files { 91 | if f.IsDir() && f.Name() != "recipients" { 92 | filter := computeBloomFilter(f.Name()) 93 | m.iIndex.AddDoc(docID, f.Name(), filter) //insert into inverted index 94 | m.fIndex.AddDoc(docID, f.Name()) //Insert into forward index 95 | docID++ 96 | } 97 | } 98 | } 99 | 100 | func LevenshteinDistance(a, b *string) int { 101 | la := len(*a) 102 | lb := len(*b) 103 | d := make([]int, la + 1) 104 | var lastdiag, olddiag, temp int 105 | 106 | for i := 1; i <= la; i++ { 107 | d[i] = i 108 | } 109 | for i := 1; i <= lb; i++ { 110 | d[0] = i 111 | lastdiag = i - 1 112 | for j := 1; j <= la; j++ { 113 | olddiag = d[j] 114 | min := d[j] + 1 115 | if (d[j - 1] + 1) < min { 116 | min = d[j - 1] + 1 117 | } 118 | if ( (*a)[j - 1] == (*b)[i - 1] ) { 119 | temp = 0 120 | } else { 121 | temp = 1 122 | } 123 | if (lastdiag + temp) < min { 124 | min = lastdiag + temp 125 | } 126 | d[j] = min 127 | lastdiag = olddiag 128 | } 129 | } 130 | return d[la] 131 | } 132 | 133 | func Min(a ...int) int { 134 | min := int(^uint(0) >> 1) // largest int 135 | for _, i := range a { 136 | if i < min { 137 | min = i 138 | } 139 | } 140 | return min 141 | } 142 | func Max(a ...int) int { 143 | max := int(0) 144 | for _, i := range a { 145 | if i > max { 146 | max = i 147 | } 148 | } 149 | return max 150 | } 151 | 152 | type indexContainer struct { 153 | iIndex *InvertedIndex 154 | fIndex *ForwardIndex 155 | } 156 | 157 | var m *indexContainer 158 | var chosenScoringFunction fn_score 159 | 160 | type RankedResults []RankedResult 161 | type ByScore struct{ RankedResults } 162 | 163 | func (s RankedResults) Len() int { return len(s) } 164 | func (s RankedResults) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 165 | func (s ByScore) Less(i, j int) bool { return s.RankedResults[i].Score > s.RankedResults[j].Score } 166 | 167 | type RankedResult struct { 168 | Word string 169 | Score float64 170 | } 171 | 172 | //This is the meat of the search. It first checks the inverted index 173 | //for matches, then filters the potentially numerous results using 174 | //the bloom filter. Finally, it ranks the word using a Levenshtein 175 | //distance. 176 | func CleoSearch(iIndex *InvertedIndex, fIndex *ForwardIndex, query string) []RankedResult { 177 | rslt := make([]RankedResult, 0, 0) 178 | 179 | candidates := iIndex.Search(query) //First get candidates from Inverted Index 180 | qBloom := computeBloomFilter(query) 181 | 182 | for _, i := range candidates { 183 | if TestBytesFromQuery(i.bloom, qBloom) == true { //Filter using Bloom Filter 184 | c := fIndex.itemAt(i.docId) //Get whole document from Forward Index 185 | score := chosenScoringFunction(query, c) //Score the Forward Index between 0-1 186 | ranked := RankedResult{c, score} 187 | rslt = append(rslt, ranked) 188 | } 189 | } 190 | return rslt 191 | } 192 | 193 | //Iterates through all of the 8 bytes (64 bits) and tests 194 | //each bit that is set to 1 in the query's filter against 195 | //the bit in the comparison's filter. If the bit is not 196 | // also 1, you do not have a match. 197 | func TestBytesFromQuery(bf int, qBloom int) bool { 198 | for i := uint(0); i < 64; i++ { 199 | //a & (1 << idx) == b & (1 << idx) 200 | if (bf&(1<> 1) 229 | NUM_BITS = 64 230 | 231 | FNV_BASIS_32 = uint32(0x811c9dc5) 232 | FNV_PRIME_32 = uint32((1 << 24) + 403) 233 | FNV_MASK_32 = uint32(^uint32(0) >> 1) 234 | ) 235 | 236 | type fn_score func(word, query string) (score float64) 237 | 238 | //The bloom filter of a word is 8 bytes in length 239 | //and has each character added separately 240 | func computeBloomFilter(s string) int { 241 | cnt := len(s) 242 | 243 | if cnt <= 0 { 244 | return 0 245 | } 246 | 247 | var filter int 248 | hash := uint64(0) 249 | 250 | for i := 0; i < cnt; i++ { 251 | c := s[i] 252 | 253 | //first hash function 254 | hash ^= uint64(0xFF & c) 255 | hash *= FNV_PRIME_64 256 | 257 | //second hash function (reduces collisions for bloom) 258 | hash ^= uint64(0xFF & (c >> 16)) 259 | hash *= FNV_PRIME_64 260 | 261 | //position of the bit mod the number of bits (8 bytes = 64 bits) 262 | bitpos := hash % NUM_BITS 263 | if bitpos < 0 { 264 | bitpos += NUM_BITS 265 | } 266 | filter = filter | (1 << bitpos) 267 | } 268 | 269 | return filter 270 | } 271 | 272 | //Inverted Index - Maps the query prefix to the matching documents 273 | type InvertedIndex map[string][]Document 274 | 275 | func NewInvertedIndex() *InvertedIndex { 276 | i := make(InvertedIndex) 277 | return &i 278 | } 279 | 280 | func (x *InvertedIndex) Size() int { 281 | return len(map[string][]Document(*x)) 282 | } 283 | 284 | func (x *InvertedIndex) AddDoc(docId int, doc string, bloom int) { 285 | for _, word := range strings.Fields(doc) { 286 | word = getPrefix(word) 287 | 288 | ref, ok := (*x)[word] 289 | if !ok { 290 | ref = nil 291 | } 292 | 293 | (*x)[word] = append(ref, Document{docId: docId, bloom: bloom}) 294 | } 295 | } 296 | 297 | func (x *InvertedIndex) Search(query string) []Document { 298 | q := getPrefix(query) 299 | 300 | ref, ok := (*x)[q] 301 | 302 | if ok { 303 | return ref 304 | } 305 | return nil 306 | } 307 | 308 | //Forward Index - Maps the document id to the document 309 | type ForwardIndex map[int]string 310 | 311 | func NewForwardIndex() *ForwardIndex { 312 | i := make(ForwardIndex) 313 | return &i 314 | } 315 | func (x *ForwardIndex) AddDoc(docId int, doc string) { 316 | for _, word := range strings.Fields(doc) { 317 | _, ok := (*x)[docId] 318 | if !ok { 319 | (*x)[docId] = word 320 | } 321 | } 322 | } 323 | func (x *ForwardIndex) itemAt(i int) string { 324 | return (*x)[i] 325 | } 326 | 327 | 328 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | * This file is part of theary. 5 | * 6 | * It uses portion of code from Go-Guerrilla SMTPd 7 | * Copyright (c) 2012 Flashmob, GuerrillaMail.com 8 | * 9 | * theary is free software: you can redistribute it and/or modify 10 | * it under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * theary is distributed in the hope that it will be useful, 15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | * GNU General Public License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public License 20 | * along with Foobar. If not, see . 21 | * 22 | */ 23 | 24 | import ( 25 | "bufio" 26 | "bytes" 27 | "crypto/md5" 28 | "crypto/rand" 29 | "crypto/tls" 30 | "encoding/base64" 31 | "encoding/hex" 32 | "encoding/json" 33 | "errors" 34 | "fmt" 35 | "github.com/sloonz/go-qprintable" 36 | 37 | //TODO : replace this C binding "github.com/sloonz/go-iconv" 38 | // By a pure go solution with go.text 39 | //"code.google.com/p/go.text/encoding" 40 | //"code.google.com/p/go.text/encoding/charmap" 41 | //"code.google.com/p/go.text/transform" 42 | 43 | "io" 44 | "io/ioutil" 45 | "log" 46 | "net" 47 | 48 | "os" 49 | "regexp" 50 | "runtime" 51 | "strconv" 52 | "strings" 53 | "time" 54 | 55 | "github.com/HouzuoGuo/tiedot/db" 56 | randomize "math/rand" 57 | "bitbucket.org/kardianos/osext" 58 | "bitbucket.org/kardianos/service" 59 | "path/filepath" 60 | ) 61 | 62 | type Client struct { 63 | state int 64 | Helo string 65 | Mail_from string 66 | Rcpt_to string 67 | read_buffer string 68 | Response string 69 | Address string 70 | Data string 71 | Subject string 72 | Hash string 73 | time int64 74 | tls_on bool 75 | conn net.Conn 76 | bufin *bufio.Reader 77 | bufout *bufio.Writer 78 | kill_time int64 79 | errors int 80 | ClientId int64 81 | savedNotify chan int 82 | } 83 | 84 | var TLSconfig *tls.Config 85 | var max_size int // max email Data size 86 | var timeout time.Duration 87 | var allowedHosts = make(map[string]bool, 15) 88 | var sem chan int // currently active clients 89 | var SaveMailChan chan *Client // workers for saving mail 90 | 91 | // defaults. Overwrite any of these in the configure() function which loads them from a json file 92 | var gConfig = make(map[string]string) 93 | var exePath, logFile, configFile, logPath, dataPath, tmplPath, staticPath string 94 | var dbEmails *db.DB 95 | 96 | var logSrv service.Logger 97 | var name = "theary" 98 | var displayName = "fake SMTP Server" 99 | var desc = "fake SMTP Server with a minimalist webmail client written in pure go" 100 | var isService bool = true 101 | 102 | func logln(level int, s string) { 103 | if gConfig["GSMTP_VERBOSE"] == "Y" { 104 | fmt.Println(s) 105 | } 106 | if level == 2 { 107 | log.Fatalf(s) 108 | } 109 | } 110 | 111 | // main runs the program as a service or as a command line tool. 112 | // Several verbs allows you to install, start, stop or remove the service. 113 | // "run" verb allows you to run the program as a command line tool. 114 | // e.g. "theary install" installs the service 115 | // e.g. "theary run" starts the program from the console (blocking) 116 | func main() { 117 | s, err := service.NewService(name, displayName, desc) 118 | if err != nil { 119 | fmt.Printf("%s unable to start: %s", displayName, err) 120 | return 121 | } 122 | logSrv = s 123 | 124 | if len(os.Args) > 1 { 125 | var err error 126 | verb := os.Args[1] 127 | switch verb { 128 | case "install": 129 | err = s.Install() 130 | if err != nil { 131 | fmt.Printf("Failed to install: %s\n", err) 132 | return 133 | } 134 | fmt.Printf("Service \"%s\" installed.\n", displayName) 135 | case "remove": 136 | err = s.Remove() 137 | if err != nil { 138 | fmt.Printf("Failed to remove: %s\n", err) 139 | return 140 | } 141 | fmt.Printf("Service \"%s\" removed.\n", displayName) 142 | case "run": 143 | isService = false 144 | doWork() 145 | case "start": 146 | err = s.Start() 147 | if err != nil { 148 | fmt.Printf("Failed to start: %s\n", err) 149 | return 150 | } 151 | fmt.Printf("Service \"%s\" started.\n", displayName) 152 | case "stop": 153 | err = s.Stop() 154 | if err != nil { 155 | fmt.Printf("Failed to stop: %s\n", err) 156 | return 157 | } 158 | fmt.Printf("Service \"%s\" stopped.\n", displayName) 159 | } 160 | return 161 | } 162 | err = s.Run(func() error { 163 | // start 164 | go doWork() 165 | return nil 166 | }, func() error { 167 | // stop 168 | stopWork() 169 | return nil 170 | }) 171 | if err != nil { 172 | s.Error(err.Error()) 173 | } 174 | } 175 | 176 | // configure sets up theary by reading the configuration file 177 | func configure() { 178 | //Set the paths of the various folder and create the non existing ones 179 | exePath, _ = osext.ExecutableFolder() 180 | configFile = filepath.Join(exePath, "conf", "conf.json") 181 | logPath = filepath.Join(exePath, "logs") 182 | dataPath = filepath.Join(exePath, "data") 183 | tmplPath = filepath.Join(exePath, "tmpl") 184 | staticPath = filepath.Join(exePath, "static") 185 | logFile = filepath.Join(logPath, "theary.log") 186 | 187 | if _, err := os.Stat(dataPath); err != nil { 188 | if os.IsNotExist(err) { 189 | os.MkdirAll(dataPath, 0666) 190 | } 191 | } 192 | if _, err := os.Stat(logPath); err != nil { 193 | if os.IsNotExist(err) { 194 | os.MkdirAll(logPath, 0666) 195 | } 196 | } 197 | 198 | //Set log to logs/theary.log 199 | f, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 200 | if err != nil { 201 | log.Fatalf("error opening file: %v", err) 202 | } 203 | defer f.Close() 204 | log.SetOutput(f) 205 | log.Println("config file", configFile) 206 | 207 | // load in the config. 208 | b, err := ioutil.ReadFile(configFile) 209 | if err != nil { 210 | log.Fatalln("Could not read config file") 211 | } 212 | var myConfig map[string]string 213 | err = json.Unmarshal(b, &myConfig) 214 | if err != nil { 215 | log.Fatalln("Could not parse config file") 216 | } 217 | for k, v := range myConfig { 218 | gConfig[k] = v 219 | } 220 | // map the allow hosts for easy lookup 221 | if arr := strings.Split(gConfig["GM_ALLOWED_HOSTS"], ","); len(arr) > 0 { 222 | for i := 0; i < len(arr); i++ { 223 | allowedHosts[arr[i]] = true 224 | } 225 | } 226 | var n int 227 | var n_err error 228 | // sem is an active clients channel used for counting clients 229 | if n, n_err = strconv.Atoi(gConfig["GM_MAX_CLIENTS"]); n_err != nil { 230 | n = 50 231 | } 232 | // currently active client list 233 | sem = make(chan int, n) 234 | // Database writing workers 235 | SaveMailChan = make(chan *Client, 5) 236 | // timeout for reads 237 | if n, n_err = strconv.Atoi(gConfig["GSMTP_TIMEOUT"]); n_err != nil { 238 | timeout = time.Duration(10) 239 | } else { 240 | timeout = time.Duration(n) 241 | } 242 | // max email size 243 | if max_size, n_err = strconv.Atoi(gConfig["GSMTP_MAX_SIZE"]); n_err != nil { 244 | max_size = 131072 245 | } 246 | 247 | log.Println("Theary is now configured") 248 | } 249 | 250 | // doWork is the actual main entry function of the application 251 | func doWork() { 252 | configure() 253 | 254 | //Open database 255 | dbEmails, _ = db.OpenDB(dataPath) 256 | randomize.Seed(time.Now().UTC().UnixNano()) 257 | 258 | //Launch cleaner 259 | duration, err := strconv.ParseInt(gConfig["CLEANER_INTERVAL"], 10, 64) 260 | interval := time.NewTicker(time.Second * time.Duration(duration)) 261 | go cleaner(interval) 262 | 263 | pubKeyFile := filepath.Join(exePath, "conf", "public.pem") 264 | privKeyFile := filepath.Join(exePath, "conf", "private.key") 265 | cert, err := tls.LoadX509KeyPair(pubKeyFile, privKeyFile) 266 | if err != nil { 267 | logln(2, fmt.Sprintf("There was a problem with loading the certificate: %s", err)) 268 | } 269 | TLSconfig = &tls.Config{Certificates: []tls.Certificate{cert}, ClientAuth: tls.VerifyClientCertIfGiven, ServerName: gConfig["GSMTP_HOST_NAME"]} 270 | TLSconfig.Rand = rand.Reader 271 | // start some savemail workers 272 | for i := 0; i < 3; i++ { 273 | go saveMail() 274 | } 275 | 276 | //Start watching modification on db folder / start cleo engine 277 | BuildIndexes(nil) 278 | watchFolderRecipients() 279 | 280 | //Setup the minimalist webmail interface 281 | if gConfig["WEBUI_MODE"] != "DISABLED" { 282 | go setup_webui() 283 | } 284 | 285 | // Start listening for SMTP connections 286 | listener, err := net.Listen("tcp", gConfig["GSTMP_LISTEN_INTERFACE"]) 287 | if err != nil { 288 | logln(2, fmt.Sprintf("Cannot listen on port, %v", err)) 289 | } else { 290 | logln(1, fmt.Sprintf("Listening on tcp %s", gConfig["GSTMP_LISTEN_INTERFACE"])) 291 | } 292 | var ClientId int64 293 | ClientId = 1 294 | for { 295 | conn, err := listener.Accept() 296 | if err != nil { 297 | logln(1, fmt.Sprintf("Accept error: %s", err)) 298 | continue 299 | } 300 | logln(1, fmt.Sprintf(" There are now "+strconv.Itoa(runtime.NumGoroutine())+" serving goroutines")) 301 | sem <- 1 // Wait for active queue to drain. 302 | go handleClient(&Client{ 303 | conn: conn, 304 | Address: conn.RemoteAddr().String(), 305 | time: time.Now().Unix(), 306 | bufin: bufio.NewReader(conn), 307 | bufout: bufio.NewWriter(conn), 308 | ClientId: ClientId, 309 | savedNotify: make(chan int), 310 | }) 311 | ClientId++ 312 | } 313 | } 314 | 315 | // stopWork stops the service 316 | func stopWork() { 317 | logInfo("I'm Stopping!") 318 | } 319 | 320 | // logInfo reports a message in the console or the system log, 321 | // depending on the execution context (console or service) 322 | func logInfo(logMessage string, a ...interface{}) { 323 | if isService { 324 | logSrv.Info(logMessage, a...) 325 | } else { 326 | log.Printf(logMessage, a...) 327 | } 328 | } 329 | 330 | // logInfo reports an error in the console or the system log, 331 | // depending on the execution context (console or service) 332 | func logFatal(logMessage string, a ...interface{}) { 333 | if isService { 334 | logSrv.Error(logMessage, a...) 335 | } else { 336 | log.Fatalf(logMessage, a...) 337 | } 338 | } 339 | 340 | // checkError checks and reports any fatal error (errors occuring before the HTTP server is listening) 341 | func checkError(err error) { 342 | if err != nil { 343 | logFatal("%v", err) 344 | } 345 | } 346 | 347 | // handleClient is a go routine that manages a SMTP client 348 | func handleClient(client *Client) { 349 | defer closeClient(client) 350 | // defer closeClient(client) 351 | greeting := "220 " + gConfig["GSMTP_HOST_NAME"] + 352 | " SMTP Guerrilla-SMTPd #" + strconv.FormatInt(client.ClientId, 10) + " (" + strconv.Itoa(len(sem)) + ") " + time.Now().Format(time.RFC1123Z) 353 | advertiseTls := "250-STARTTLS\r\n" 354 | for i := 0; i < 100; i++ { 355 | switch client.state { 356 | case 0: 357 | ResponseAdd(client, greeting) 358 | client.state = 1 359 | case 1: 360 | input, err := readSmtp(client) 361 | if err != nil { 362 | logln(1, fmt.Sprintf("Read error: %v", err)) 363 | if err == io.EOF { 364 | // client closed the connection already 365 | return 366 | } 367 | if neterr, ok := err.(net.Error); ok && neterr.Timeout() { 368 | // too slow, timeout 369 | return 370 | } 371 | break 372 | } 373 | input = strings.Trim(input, " \n\r") 374 | cmd := strings.ToUpper(input) 375 | switch { 376 | case strings.Index(cmd, "HELO") == 0: 377 | if len(input) > 5 { 378 | client.Helo = input[5:] 379 | } 380 | ResponseAdd(client, "250 "+ gConfig["GSMTP_HOST_NAME"] + " Hello ") 381 | case strings.Index(cmd, "EHLO") == 0: 382 | if len(input) > 5 { 383 | client.Helo = input[5:] 384 | } 385 | ResponseAdd(client, "250-"+gConfig["GSMTP_HOST_NAME"]+ " Hello " + client.Helo+"["+client.Address+"]"+"\r\n"+"250-SIZE "+gConfig["GSMTP_MAX_SIZE"]+"\r\n"+advertiseTls+"250 HELP") 386 | case strings.Index(cmd, "MAIL FROM:") == 0: 387 | if len(input) > 10 { 388 | client.Mail_from = input[10:] 389 | } 390 | ResponseAdd(client, "250 Ok") 391 | case strings.Index(cmd, "XCLIENT") == 0: 392 | // Nginx sends this 393 | // XCLIENT ADDR=212.96.64.216 NAME=[UNAVAILABLE] 394 | client.Address = input[13:] 395 | client.Address = client.Address[0:strings.Index(client.Address, " ")] 396 | fmt.Println("client Address:[" + client.Address + "]") 397 | ResponseAdd(client, "250 OK") 398 | case strings.Index(cmd, "RCPT TO:") == 0: 399 | if len(input) > 8 { 400 | client.Rcpt_to = input[8:] 401 | } 402 | ResponseAdd(client, "250 Accepted") 403 | case strings.Index(cmd, "NOOP") == 0: 404 | ResponseAdd(client, "250 OK") 405 | case strings.Index(cmd, "RSET") == 0: 406 | client.Mail_from = "" 407 | client.Rcpt_to = "" 408 | ResponseAdd(client, "250 OK") 409 | case strings.Index(cmd, "DATA") == 0: 410 | ResponseAdd(client, "354 Enter message, ending with \".\" on a line by itself") 411 | client.state = 2 412 | case (strings.Index(cmd, "STARTTLS") == 0) && !client.tls_on: 413 | ResponseAdd(client, "220 Ready to start TLS") 414 | // go to start TLS state 415 | client.state = 3 416 | case strings.Index(cmd, "QUIT") == 0: 417 | ResponseAdd(client, "221 Bye") 418 | killClient(client) 419 | default: 420 | ResponseAdd(client, fmt.Sprintf("500 unrecognized command")) 421 | client.errors++ 422 | if client.errors > 3 { 423 | ResponseAdd(client, fmt.Sprintf("500 Too many unrecognized commands")) 424 | killClient(client) 425 | } 426 | } 427 | case 2: 428 | var err error 429 | client.Data, err = readSmtp(client) 430 | if err == nil { 431 | // to do: timeout when adding to SaveMailChan 432 | // place on the channel so that one of the save mail workers can pick it up 433 | SaveMailChan <- client 434 | // wait for the save to complete 435 | status := <-client.savedNotify 436 | 437 | if status == 1 { 438 | ResponseAdd(client, "250 OK : queued as "+client.Hash) 439 | } else { 440 | ResponseAdd(client, "554 Error: transaction failed, blame it on the weather") 441 | } 442 | } else { 443 | logln(1, fmt.Sprintf("Data read error: %v", err)) 444 | } 445 | client.state = 1 446 | case 3: 447 | // upgrade to TLS 448 | var tlsConn *tls.Conn 449 | tlsConn = tls.Server(client.conn, TLSconfig) 450 | err := tlsConn.Handshake() // not necessary to call here, but might as well 451 | if err == nil { 452 | client.conn = net.Conn(tlsConn) 453 | client.bufin = bufio.NewReader(client.conn) 454 | client.bufout = bufio.NewWriter(client.conn) 455 | client.tls_on = true 456 | } else { 457 | logln(1, fmt.Sprintf("Could not TLS handshake:%v", err)) 458 | } 459 | advertiseTls = "" 460 | client.state = 1 461 | } 462 | // Send a Response back to the client 463 | err := ResponseWrite(client) 464 | if err != nil { 465 | if err == io.EOF { 466 | // client closed the connection already 467 | return 468 | } 469 | if neterr, ok := err.(net.Error); ok && neterr.Timeout() { 470 | // too slow, timeout 471 | return 472 | } 473 | } 474 | if client.kill_time > 1 { 475 | return 476 | } 477 | } 478 | } 479 | 480 | // ResponseAdd adds a line to the response sent to the SMTP client 481 | func ResponseAdd(client *Client, line string) { 482 | client.Response = line + "\r\n" 483 | } 484 | 485 | // closeClient closes the connection of the SMTP client 486 | func closeClient(client *Client) { 487 | client.conn.Close() 488 | <-sem // Done; enable next client to run. 489 | } 490 | 491 | // killClient kills the connection of the SMTP client 492 | func killClient(client *Client) { 493 | client.kill_time = time.Now().Unix() 494 | } 495 | 496 | // readSmtp reads the SMTP message sent by client 497 | func readSmtp(client *Client) (input string, err error) { 498 | var reply string 499 | // Command state terminator by default 500 | suffix := "\r\n" 501 | if client.state == 2 { 502 | // Data state 503 | suffix = "\r\n.\r\n" 504 | } 505 | for err == nil { 506 | client.conn.SetDeadline(time.Now().Add(timeout * time.Second)) 507 | reply, err = client.bufin.ReadString('\n') 508 | if reply != "" { 509 | input = input + reply 510 | if len(input) > max_size { 511 | err = errors.New("Maximum Data size exceeded (" + strconv.Itoa(max_size) + ")") 512 | return input, err 513 | } 514 | if client.state == 2 { 515 | // Extract the Subject while we are at it. 516 | scanSubject(client, reply) 517 | } 518 | } 519 | if err != nil { 520 | break 521 | } 522 | if strings.HasSuffix(input, suffix) { 523 | break 524 | } 525 | } 526 | return input, err 527 | } 528 | 529 | // scanSubject scans the Data part for a Subject line. Can be a multi-line 530 | func scanSubject(client *Client, reply string) { 531 | if client.Subject == "" && (len(reply) > 8) { 532 | test := strings.ToUpper(reply[0:9]) 533 | if i := strings.Index(test, "SUBJECT: "); i == 0 { 534 | // first line with \r\n 535 | client.Subject = reply[9:] 536 | } 537 | } else if strings.HasSuffix(client.Subject, "\r\n") { 538 | // chop off the \r\n 539 | client.Subject = client.Subject[0 : len(client.Subject)-2] 540 | if (strings.HasPrefix(reply, " ")) || (strings.HasPrefix(reply, "\t")) { 541 | // Subject is multi-line 542 | client.Subject = client.Subject + reply[1:] 543 | } 544 | } 545 | } 546 | 547 | // ResponseWrite write a response to the STMP client 548 | func ResponseWrite(client *Client) (err error) { 549 | var size int 550 | client.conn.SetDeadline(time.Now().Add(timeout * time.Second)) 551 | size, err = client.bufout.WriteString(client.Response) 552 | client.bufout.Flush() 553 | client.Response = client.Response[size:] 554 | return err 555 | } 556 | 557 | // saveMail receives values from the channel repeatedly until it is closed. 558 | func saveMail() { 559 | for { 560 | client := <-SaveMailChan 561 | client.Subject = mimeHeaderDecode(client.Subject) 562 | client.Hash = md5hex(client.Rcpt_to + client.Mail_from + client.Subject + strconv.FormatInt(time.Now().UnixNano(), 10)) 563 | to := strings.Replace(client.Rcpt_to, "<", "", -1) 564 | to = strings.Replace(to, ">", "", -1) 565 | from := strings.Replace(client.Mail_from, "<", "", -1) 566 | from = strings.Replace(from, ">", "", -1) 567 | timestamp := time.Now().Format("20060102150405.000000000") 568 | 569 | createIfNotIndB(to) 570 | emails := dbEmails.Use(to) 571 | _, err := emails.Insert(map[string]interface{}{ 572 | "timestamp": timestamp, 573 | "from": from, 574 | "subject": client.Subject, 575 | "data": client.Data, 576 | "address": client.Address}) 577 | if err != nil { 578 | panic(err) 579 | } 580 | //fmt.Println("++++", id, client.Rcpt_to, client.Subject) 581 | client.savedNotify <- 1 582 | } 583 | } 584 | 585 | // Decode strings in Mime header format 586 | // eg. =?ISO-2022-JP?B?GyRCIVo9dztSOWJAOCVBJWMbKEI=?= 587 | func mimeHeaderDecode(str string) string { 588 | reg, _ := regexp.Compile(`=\?(.+?)\?([QBqp])\?(.+?)\?=`) 589 | matched := reg.FindAllStringSubmatch(str, -1) 590 | var charset, encoding, payload string 591 | if matched != nil { 592 | for i := 0; i < len(matched); i++ { 593 | if len(matched[i]) > 2 { 594 | charset = matched[i][1] 595 | encoding = strings.ToUpper(matched[i][2]) 596 | payload = matched[i][3] 597 | switch encoding { 598 | case "B": 599 | str = strings.Replace(str, matched[i][0], mailTransportDecode(payload, "base64", charset), 1) 600 | case "Q": 601 | str = strings.Replace(str, matched[i][0], mailTransportDecode(payload, "quoted-printable", charset), 1) 602 | } 603 | } 604 | } 605 | } 606 | return str 607 | } 608 | 609 | // decode from 7bit to 8bit UTF-8 610 | // encoding_type can be "base64" or "quoted-printable" 611 | func mailTransportDecode(str string, encoding_type string, charset string) string { 612 | if charset == "" { 613 | charset = "UTF-8" 614 | } else { 615 | charset = strings.ToUpper(charset) 616 | } 617 | if encoding_type == "base64" { 618 | str = fromBase64(str) 619 | } else if encoding_type == "quoted-printable" { 620 | str = fromQuotedP(str) 621 | } 622 | if charset != "UTF-8" { 623 | charset = fixCharset(charset) 624 | // eg. charset can be "ISO-2022-JP" 625 | 626 | //TODO 627 | //convstr, err := iconv.Conv(str, "UTF-8", charset) 628 | 629 | //sr := strings.NewReader(str) 630 | //tr := transform.NewReader(sr, charmap.Windows1252.NewDecoder()) 631 | 632 | //CodePage437 633 | //CodePage866 634 | //ISO8859_2 635 | 636 | /*if err == nil { 637 | return convstr 638 | }*/ 639 | } 640 | return str 641 | } 642 | 643 | // fromBase64 decodes a base64 encoded string 644 | func fromBase64(Data string) string { 645 | buf := bytes.NewBufferString(Data) 646 | decoder := base64.NewDecoder(base64.StdEncoding, buf) 647 | res, _ := ioutil.ReadAll(decoder) 648 | return string(res) 649 | } 650 | 651 | // fromQuotedP decodes a quoted string 652 | func fromQuotedP(Data string) string { 653 | buf := bytes.NewBufferString(Data) 654 | decoder := qprintable.NewDecoder(qprintable.BinaryEncoding, buf) 655 | res, _ := ioutil.ReadAll(decoder) 656 | return string(res) 657 | } 658 | 659 | // fixCharset fixes charset 660 | func fixCharset(charset string) string { 661 | reg, _ := regexp.Compile(`[_:.\/\\]`) 662 | fixed_charset := reg.ReplaceAllString(charset, "-") 663 | // Fix charset 664 | // borrowed from http://squirrelmail.svn.sourceforge.net/viewvc/squirrelmail/trunk/squirrelmail/include/languages.php?revision=13765&view=markup 665 | // OE ks_c_5601_1987 > cp949 666 | fixed_charset = strings.Replace(fixed_charset, "ks-c-5601-1987", "cp949", -1) 667 | // Moz x-euc-tw > euc-tw 668 | fixed_charset = strings.Replace(fixed_charset, "x-euc", "euc", -1) 669 | // Moz x-windows-949 > cp949 670 | fixed_charset = strings.Replace(fixed_charset, "x-windows_", "cp", -1) 671 | // windows-125x and cp125x charsets 672 | fixed_charset = strings.Replace(fixed_charset, "windows-", "cp", -1) 673 | // ibm > cp 674 | fixed_charset = strings.Replace(fixed_charset, "ibm", "cp", -1) 675 | // iso-8859-8-i -> iso-8859-8 676 | fixed_charset = strings.Replace(fixed_charset, "iso-8859-8-i", "iso-8859-8", -1) 677 | if charset != fixed_charset { 678 | return fixed_charset 679 | } 680 | return charset 681 | } 682 | 683 | // md5hex returns a string containing the MD5 hash of the content passed as a parameter 684 | func md5hex(str string) string { 685 | h := md5.New() 686 | h.Write([]byte(str)) 687 | sum := h.Sum([]byte{}) 688 | return hex.EncodeToString(sum) 689 | } 690 | -------------------------------------------------------------------------------- /static/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/static/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /static/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/static/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /static/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/static/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /static/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/static/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /static/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/static/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /static/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/static/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /static/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/static/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/static/apple-touch-icon.png -------------------------------------------------------------------------------- /static/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | #da532c 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/static/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon-196x196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/static/favicon-196x196.png -------------------------------------------------------------------------------- /static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/static/favicon-32x32.png -------------------------------------------------------------------------------- /static/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/static/favicon-96x96.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/static/favicon.ico -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/static/logo.png -------------------------------------------------------------------------------- /static/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/static/mstile-150x150.png -------------------------------------------------------------------------------- /static/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/static/mstile-310x150.png -------------------------------------------------------------------------------- /static/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbalet/theary/765daa6b69a7bdb61720682fe6549986a68105e9/static/mstile-70x70.png -------------------------------------------------------------------------------- /tmpl/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ .Title }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 |
 
33 |
34 | 35 |
36 |
37 |   Theary, a fake SMTP server 38 |
39 |
40 | 41 |
42 | 43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
IDTimestampFromSubjectAddress
64 | 65 | 66 | 67 | 78 | 79 | 91 | 92 | 104 | 105 | 246 | 247 |
248 |
249 | 250 | 251 |
252 |
253 |

© 2014 Benjamin BALET · Theary is a fake SMTP server with a minimalist webmail client

254 |
255 | 256 |
257 | 258 | 259 | -------------------------------------------------------------------------------- /webmail.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | * This file is part of theary. 5 | * 6 | * theary is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * theary is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | * 19 | */ 20 | 21 | import ( 22 | "net/http" 23 | "fmt" 24 | "github.com/gorilla/mux" 25 | "github.com/HouzuoGuo/tiedot/db" 26 | "encoding/json" 27 | "strconv" 28 | ) 29 | 30 | type dataTable struct { 31 | //Echo int `json:"sEcho"` 32 | TotalRecords int `json:"iTotalRecords"` 33 | TotalDisplayRecords int `json:"iTotalDisplayRecords"` 34 | Rows [][]string `json:"aaData"` 35 | } 36 | 37 | type EmailTable struct { 38 | Cells []string `json:",string"` 39 | } 40 | 41 | // homeView displays a minimalist webmail client (renders template) 42 | func homeView(w http.ResponseWriter, r *http.Request) { 43 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 44 | p := Page{Title: "Test"} 45 | err := tmpl.Execute(w, p) 46 | checkHttpError(err, w) 47 | } 48 | 49 | // listMailsWS list the received e-mails from db (returns JSON) 50 | func listMailsWS(w http.ResponseWriter, r *http.Request) { 51 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 52 | vars := mux.Vars(r) 53 | recipient := vars["recipient"] 54 | emails := dbEmails.Use(recipient) 55 | 56 | var mailRecords dataTable 57 | //mailRecords.Echo = 3 58 | 59 | queryStr := `"all"` 60 | var query interface{} 61 | var record map[string]interface{} 62 | json.Unmarshal([]byte(queryStr), &query) 63 | queryResult := make(map[uint64]struct{}) 64 | err := db.EvalQuery(query, emails, &queryResult) 65 | checkHttpError(err, w) 66 | 67 | for id := range queryResult { 68 | emails.Read(id, &record) 69 | mailRecords.TotalRecords++ 70 | mailRecords.TotalDisplayRecords++ 71 | row := []string{strconv.FormatUint(id, 10), 72 | record["timestamp"].(string), 73 | record["from"].(string), 74 | record["subject"].(string), 75 | record["address"].(string)} 76 | //var rowEntry EmailTable 77 | //rowEntry.Cells = row 78 | mailRecords.Rows = append(mailRecords.Rows, row) 79 | } 80 | jsonString, err := json.Marshal(mailRecords) 81 | checkHttpError(err, w) 82 | fmt.Fprintf(w, "%s", jsonString) 83 | } 84 | 85 | // getMailWS returns the e-mails details from DB (returns JSON) 86 | func getMailWS(w http.ResponseWriter, r *http.Request) { 87 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 88 | vars := mux.Vars(r) 89 | recipient := vars["recipient"] 90 | var id uint64 91 | id, err := strconv.ParseUint(vars["id"], 10, 64) 92 | checkHttpError(err, w) 93 | emails := dbEmails.Use(recipient) 94 | var record map[string]interface{} 95 | emails.Read(id, &record) 96 | encoder := json.NewEncoder(w) 97 | err = encoder.Encode(record["data"].(string)) 98 | checkHttpError(err, w) 99 | } 100 | 101 | // checkRecipientWS checks if a recipient exists or not (returns JSON) 102 | func checkRecipientWS(w http.ResponseWriter, r *http.Request) { 103 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 104 | vars := mux.Vars(r) 105 | recipient := vars["recipient"] 106 | encoder := json.NewEncoder(w) 107 | err := encoder.Encode(existsIndB(recipient)) 108 | checkHttpError(err, w) 109 | } 110 | -------------------------------------------------------------------------------- /webui.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | * This file is part of theary. 5 | * 6 | * theary is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * theary is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Foobar. If not, see . 18 | * 19 | */ 20 | 21 | import ( 22 | "io" 23 | "strings" 24 | "compress/gzip" 25 | "log" 26 | "net" 27 | "net/http" 28 | "net/http/fcgi" 29 | "html/template" 30 | "path/filepath" 31 | "github.com/gorilla/mux" 32 | ) 33 | 34 | //List of HTML templates 35 | var tmpl *template.Template 36 | 37 | //Data passed to a template 38 | type Page struct { 39 | Title string 40 | } 41 | 42 | // Write is a closure for compressing the HTTP output of a web handler function 43 | func makeHandler(fn func(http.ResponseWriter, *http.Request)) http.HandlerFunc { 44 | return func(w http.ResponseWriter, r *http.Request) { 45 | //w.Header().Set("Access-Control-Allow-Origin", "*") 46 | if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { 47 | fn(w, r) 48 | return 49 | } 50 | w.Header().Set("Content-Encoding", "gzip") 51 | gz := gzip.NewWriter(w) 52 | defer gz.Close() 53 | gzr := gzipResponseWriter{Writer: gz, ResponseWriter: w} 54 | fn(gzr, r) 55 | } 56 | } 57 | 58 | type gzipResponseWriter struct { 59 | io.Writer 60 | http.ResponseWriter 61 | } 62 | 63 | // Write implements a writer for compressing the HTTP output 64 | func (w gzipResponseWriter) Write(b []byte) (int, error) { 65 | if "" == w.Header().Get("Content-Type") { 66 | // If no content type, apply sniffing algorithm to un-gzipped body. 67 | w.Header().Set("Content-Type", http.DetectContentType(b)) 68 | } 69 | return w.Writer.Write(b) 70 | } 71 | 72 | // setup_webui sets up the webui. It can be served by an embedded webserver or thru FastCGI 73 | func setup_webui() { 74 | tmpl = template.Must(template.ParseFiles(filepath.Join(tmplPath, "home.html"))) 75 | 76 | r := mux.NewRouter() 77 | r.HandleFunc("/", makeHandler(homeView)) 78 | r.HandleFunc("/cleo/{query}", makeHandler(searchHandler)) 79 | r.HandleFunc("/recipient/{recipient}", makeHandler(checkRecipientWS)) 80 | r.HandleFunc("/mails/{recipient}", makeHandler(listMailsWS)) 81 | r.HandleFunc("/mails/{recipient}/{id}", makeHandler(getMailWS)) 82 | r.PathPrefix("/").Handler(http.FileServer(http.Dir(staticPath))) 83 | 84 | var err error 85 | switch strings.ToUpper(gConfig["WEBUI_MODE"]) { 86 | case "LOCAL": // Run as a local web server 87 | log.Println("Run as a local web server") 88 | err = http.ListenAndServe(gConfig["WEBUI_SERVE"], r) 89 | case "TCP": // Serve as FCGI via TCP 90 | log.Println("FCGI via TCP.") 91 | listener, err := net.Listen("tcp", gConfig["WEBUI_SERVE"]) 92 | if err != nil { 93 | log.Fatal(err) 94 | } 95 | defer listener.Close() 96 | err = fcgi.Serve(listener, r) 97 | case "UNIX": // Run as FCGI via UNIX socket 98 | log.Println("FCGI via UNIX socket") 99 | listener, err := net.Listen("unix", gConfig["WEBUI_SERVE"]) 100 | if err != nil { 101 | log.Fatal(err) 102 | } 103 | defer listener.Close() 104 | err = fcgi.Serve(listener, r) 105 | } 106 | if err != nil { 107 | log.Fatal(err) 108 | } 109 | } 110 | 111 | // checkHttpError checks and reports any fatal error. Display an HTTP-500 page 112 | func checkHttpError(err error, w http.ResponseWriter) { 113 | if err != nil { 114 | http.Error(w, err.Error(), http.StatusInternalServerError) 115 | log.Fatal("%v", err) 116 | } 117 | } 118 | --------------------------------------------------------------------------------