OpenSMTPD filter for flatfile mail queue archiving
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

326 lines
6.6KB

  1. //
  2. // Copyright (c) 2019 Björn Kalkbrenner <terminar@cyberphoria.org>
  3. // (c) 2019 Gilles Chehade <gilles@poolp.org>
  4. //
  5. // Permission to use, copy, modify, and distribute this software for any
  6. // purpose with or without fee is hereby granted, provided that the above
  7. // copyright notice and this permission notice appear in all copies.
  8. //
  9. // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  10. // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  11. // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  12. // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  13. // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  14. // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  15. // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  16. //
  17. package main
  18. import (
  19. "bufio"
  20. "fmt"
  21. "os"
  22. "strings"
  23. "log"
  24. "flag"
  25. )
  26. var version string
  27. var tmpFile *os.File
  28. var bufWriter *bufio.Writer
  29. type tx struct {
  30. rcptTo []string
  31. action string
  32. response string
  33. archive archiveStorage
  34. }
  35. type session struct {
  36. id string
  37. rdns string
  38. src string
  39. heloName string
  40. userName string
  41. mtaName string
  42. tx tx
  43. }
  44. var sessions = make(map[string]*session)
  45. var reporters = map[string]func(*session, []string){
  46. "link-connect": linkConnect,
  47. "link-disconnect": linkDisconnect,
  48. "link-greeting": linkGreeting,
  49. "link-identify": linkIdentify,
  50. "link-auth": linkAuth,
  51. "tx-reset": txReset,
  52. "tx-begin": txBegin,
  53. "tx-mail": txMail,
  54. "tx-rcpt": txRcpt,
  55. "tx-data": txData,
  56. "tx-rollback": txRollback,
  57. "tx-envelope": txEnvelope,
  58. "timeout": sessionTimeout,
  59. }
  60. var filters = map[string]func(*session, []string){
  61. "data-line": dataLine,
  62. }
  63. func systemLog(text string) {
  64. fmt.Fprintln(os.Stderr, text)
  65. }
  66. func sessionTimeout(s *session, params []string) {
  67. log.Print("Session timeout for: " + s.id)
  68. }
  69. func linkConnect(s *session, params []string) {
  70. if len(params) != 4 {
  71. log.Fatal("invalid input, shouldn't happen")
  72. }
  73. s.rdns = params[0]
  74. s.src = params[2]
  75. }
  76. func linkDisconnect(s *session, params []string) {
  77. if len(params) != 0 {
  78. log.Fatal("invalid input, shouldn't happen")
  79. }
  80. delete(sessions, s.id)
  81. }
  82. func linkGreeting(s *session, params []string) {
  83. if len(params) != 1 {
  84. log.Fatal("invalid input, shouldn't happen")
  85. }
  86. s.mtaName = params[0]
  87. }
  88. func linkIdentify(s *session, params []string) {
  89. if len(params) != 2 {
  90. log.Fatal("invalid input, shouldn't happen")
  91. }
  92. s.heloName = params[1]
  93. }
  94. func linkAuth(s *session, params []string) {
  95. if len(params) != 2 {
  96. log.Fatal("invalid input, shouldn't happen")
  97. }
  98. if params[1] != "pass" {
  99. return
  100. }
  101. s.userName = params[0]
  102. }
  103. func txReset(s *session, params []string) {
  104. if len(params) != 1 {
  105. log.Print("message-id is missing, this may happen")
  106. }
  107. if err := s.tx.archive.Close(); err != nil {
  108. systemLog("ERROR: " + err.Error())
  109. }
  110. s.tx = tx{}
  111. }
  112. func txBegin(s *session, params []string) {
  113. if len(params) != 1 {
  114. log.Fatal("invalid input, shouldn't happen")
  115. }
  116. msgid := params[0]
  117. fname := s.id + "." + msgid
  118. if err := s.tx.archive.Open(fname); err != nil {
  119. systemLog("ERROR: can't create new file in " + archiveStoragePath + ". " + err.Error())
  120. }
  121. meta := s.tx.archive.Meta
  122. meta("SESSIONID=" + s.id)
  123. meta("MSGID=" + msgid)
  124. meta("MTANAME=" + s.mtaName)
  125. meta("HELONAME=" + s.heloName)
  126. meta("USERNAME=" + s.userName)
  127. meta("RDNS=" + s.rdns)
  128. meta("SRC=" + s.src)
  129. }
  130. func txMail(s *session, params []string) {
  131. if len(params) != 3 {
  132. log.Fatal("invalid input, shouldn't happen")
  133. }
  134. if params[2] != "ok" {
  135. return
  136. }
  137. s.tx.archive.Meta("FROM=" + params[1])
  138. }
  139. func txRcpt(s *session, params []string) {
  140. if len(params) != 3 {
  141. log.Fatal("invalid input, shouldn't happen")
  142. }
  143. if params[2] != "ok" {
  144. return
  145. }
  146. s.tx.rcptTo = append(s.tx.rcptTo, params[1])
  147. }
  148. func txEnvelope(s *session, params []string) {
  149. if len(params) != 2 {
  150. log.Fatal("invalid input, shouldn't happen")
  151. }
  152. s.tx.archive.Meta("ENVELOPEID=" + params[1])
  153. }
  154. func txRollback(s *session, params []string) {
  155. if len(params) != 1 {
  156. log.Fatal("invalid input, shouldn't happen")
  157. }
  158. s.tx.archive.Meta("STATE=REJECTED")
  159. }
  160. func txData(s *session, params []string) {
  161. if len(params) != 2 {
  162. log.Fatal("invalid input, shouldn't happen")
  163. }
  164. s.tx.archive.Meta("RESULT=" + params[1])
  165. if len(s.tx.rcptTo) > 0 {
  166. s.tx.archive.Meta("TO=" + strings.Join(s.tx.rcptTo[:], ","))
  167. }
  168. }
  169. func dataLine(s *session, params []string) {
  170. if len(params) < 2 {
  171. log.Fatal("invalid input, shouldn't happen")
  172. }
  173. token := params[0]
  174. line := strings.Join(params[1:], "|")
  175. // Input is raw SMTP data - unescape leading dots and write to archive
  176. s.tx.archive.Data(strings.TrimPrefix(line, "."))
  177. //just relay the line as received
  178. if version < "0.5" {
  179. fmt.Printf("filter-dataline|%s|%s|%s\n", token, s.id, line)
  180. } else {
  181. fmt.Printf("filter-dataline|%s|%s|%s\n", s.id, token, line)
  182. }
  183. }
  184. func filterInit() {
  185. for k := range reporters {
  186. fmt.Printf("register|report|smtp-in|%s\n", k)
  187. }
  188. for k := range filters {
  189. fmt.Printf("register|filter|smtp-in|%s\n", k)
  190. }
  191. fmt.Println("register|ready")
  192. }
  193. func writeLine(s *session, token string, line string) {
  194. prefix := ""
  195. // Output raw SMTP data - escape leading dots.
  196. if strings.HasPrefix(line, ".") {
  197. prefix = "."
  198. }
  199. if version < "0.5" {
  200. fmt.Printf("filter-dataline|%s|%s|%s%s\n", token, s.id, prefix, line)
  201. } else {
  202. fmt.Printf("filter-dataline|%s|%s|%s%s\n", s.id, token, prefix, line)
  203. }
  204. }
  205. func trigger(actions map[string]func(*session, []string), atoms []string) {
  206. if atoms[4] == "link-connect" {
  207. // special case to simplify subsequent code
  208. s := session{}
  209. s.id = atoms[5]
  210. sessions[s.id] = &s
  211. }
  212. s := sessions[atoms[5]]
  213. if v, ok := actions[atoms[4]]; ok {
  214. v(s, atoms[6:])
  215. } else {
  216. os.Exit(1)
  217. }
  218. }
  219. func skipConfig(scanner *bufio.Scanner) {
  220. for {
  221. if !scanner.Scan() {
  222. os.Exit(0)
  223. }
  224. line := scanner.Text()
  225. if line == "config|ready" {
  226. return
  227. }
  228. }
  229. }
  230. func main() {
  231. fArg := flag.Bool("f",false,"Use flat filesystem path storage instead of <path>/YYYY-MM/DD")
  232. flag.Parse()
  233. if flag.Arg(0) == "" {
  234. log.Fatal("Archive storage path not given as last parameter (e.g. /var/db/mail-archive)")
  235. os.Exit(0)
  236. }
  237. archiveStorageFlat = *fArg;
  238. archiveStoragePath = flag.Arg(0)
  239. scanner := bufio.NewScanner(os.Stdin)
  240. skipConfig(scanner)
  241. filterInit()
  242. for {
  243. if !scanner.Scan() {
  244. os.Exit(0)
  245. }
  246. atoms := strings.Split(scanner.Text(), "|")
  247. if len(atoms) < 6 {
  248. os.Exit(1)
  249. }
  250. version = atoms[1]
  251. switch atoms[0] {
  252. case "report":
  253. trigger(reporters, atoms)
  254. case "filter":
  255. trigger(filters, atoms)
  256. default:
  257. os.Exit(1)
  258. }
  259. }
  260. }