config.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. package main
  2. import (
  3. "bufio"
  4. "flag"
  5. "fmt"
  6. "io"
  7. "net"
  8. "os"
  9. "regexp"
  10. "strings"
  11. "time"
  12. "github.com/peterbourgon/ff/v3"
  13. "github.com/sirupsen/logrus"
  14. )
  15. var (
  16. appVersion = "unknown"
  17. buildTime = "unknown"
  18. )
  19. var (
  20. flagset = flag.NewFlagSet("smtprelay", flag.ContinueOnError)
  21. // config flags
  22. logFile = flagset.String("logfile", "", "Path to logfile")
  23. logFormat = flagset.String("log_format", "default", "Log output format")
  24. logLevel = flagset.String("log_level", "info", "Minimum log level to output")
  25. hostName = flagset.String("hostname", "localhost.localdomain", "Server hostname")
  26. welcomeMsg = flagset.String("welcome_msg", "", "Welcome message for SMTP session")
  27. listenStr = flagset.String("listen", "127.0.0.1:25 [::1]:25", "Address and port to listen for incoming SMTP")
  28. localCert = flagset.String("local_cert", "", "SSL certificate for STARTTLS/TLS")
  29. localKey = flagset.String("local_key", "", "SSL private key for STARTTLS/TLS")
  30. localForceTLS = flagset.Bool("local_forcetls", false, "Force STARTTLS (needs local_cert and local_key)")
  31. readTimeoutStr = flagset.String("read_timeout", "60s", "Socket timeout for read operations")
  32. writeTimeoutStr = flagset.String("write_timeout", "60s", "Socket timeout for write operations")
  33. dataTimeoutStr = flagset.String("data_timeout", "5m", "Socket timeout for DATA command")
  34. maxConnections = flagset.Int("max_connections", 100, "Max concurrent connections, use -1 to disable")
  35. maxMessageSize = flagset.Int("max_message_size", 10240000, "Max message size in bytes")
  36. maxRecipients = flagset.Int("max_recipients", 100, "Max RCPT TO calls for each envelope")
  37. allowedNetsStr = flagset.String("allowed_nets", "127.0.0.0/8 ::1/128", "Networks allowed to send mails")
  38. allowedSenderStr = flagset.String("allowed_sender", "", "Regular expression for valid FROM EMail addresses")
  39. allowedRecipStr = flagset.String("allowed_recipients", "", "Regular expression for valid TO EMail addresses")
  40. allowedUsers = flagset.String("allowed_users", "", "Path to file with valid users/passwords")
  41. command = flagset.String("command", "", "Path to pipe command")
  42. remotesStr = flagset.String("remotes", "", "Outgoing SMTP servers")
  43. strictSender = flagset.Bool("strict_sender", false, "Use only SMTP servers with Sender matches to From")
  44. // additional flags
  45. _ = flagset.String("config", "", "Path to config file (ini format)")
  46. versionInfo = flagset.Bool("version", false, "Show version information")
  47. // internal
  48. listenAddrs = []protoAddr{}
  49. readTimeout time.Duration
  50. writeTimeout time.Duration
  51. dataTimeout time.Duration
  52. allowedNets = []*net.IPNet{}
  53. allowedSender *regexp.Regexp
  54. allowedRecipients *regexp.Regexp
  55. remotes = []*Remote{}
  56. )
  57. func localAuthRequired() bool {
  58. return *allowedUsers != ""
  59. }
  60. func setupAllowedNetworks() {
  61. for _, netstr := range splitstr(*allowedNetsStr, ' ') {
  62. baseIP, allowedNet, err := net.ParseCIDR(netstr)
  63. if err != nil {
  64. log.WithField("netstr", netstr).
  65. WithError(err).
  66. Fatal("Invalid CIDR notation in allowed_nets")
  67. }
  68. // Reject any network specification where any host bits are set,
  69. // meaning the address refers to a host and not a network.
  70. if !allowedNet.IP.Equal(baseIP) {
  71. log.WithFields(logrus.Fields{
  72. "given_net": netstr,
  73. "proper_net": allowedNet,
  74. }).Fatal("Invalid network in allowed_nets (host bits set)")
  75. }
  76. allowedNets = append(allowedNets, allowedNet)
  77. }
  78. }
  79. func setupAllowedPatterns() {
  80. var err error
  81. if *allowedSenderStr != "" {
  82. allowedSender, err = regexp.Compile(*allowedSenderStr)
  83. if err != nil {
  84. log.WithField("allowed_sender", *allowedSenderStr).
  85. WithError(err).
  86. Fatal("allowed_sender pattern invalid")
  87. }
  88. }
  89. if *allowedRecipStr != "" {
  90. allowedRecipients, err = regexp.Compile(*allowedRecipStr)
  91. if err != nil {
  92. log.WithField("allowed_recipients", *allowedRecipStr).
  93. WithError(err).
  94. Fatal("allowed_recipients pattern invalid")
  95. }
  96. }
  97. }
  98. func setupRemotes() {
  99. logger := log.WithField("remotes", *remotesStr)
  100. if *remotesStr != "" {
  101. for _, remoteURL := range strings.Split(*remotesStr, " ") {
  102. r, err := ParseRemote(remoteURL)
  103. if err != nil {
  104. logger.Fatal(fmt.Sprintf("error parsing url: '%s': %v", remoteURL, err))
  105. }
  106. remotes = append(remotes, r)
  107. }
  108. }
  109. }
  110. type protoAddr struct {
  111. protocol string
  112. address string
  113. }
  114. func splitProto(s string) protoAddr {
  115. idx := strings.Index(s, "://")
  116. if idx == -1 {
  117. return protoAddr{
  118. address: s,
  119. }
  120. }
  121. return protoAddr{
  122. protocol: s[0:idx],
  123. address: s[idx+3:],
  124. }
  125. }
  126. func setupListeners() {
  127. for _, listenAddr := range strings.Split(*listenStr, " ") {
  128. pa := splitProto(listenAddr)
  129. if localAuthRequired() && pa.protocol == "" {
  130. log.WithField("address", pa.address).
  131. Fatal("Local authentication (via allowed_users file) " +
  132. "not allowed with non-TLS listener")
  133. }
  134. listenAddrs = append(listenAddrs, pa)
  135. }
  136. }
  137. func setupTimeouts() {
  138. var err error
  139. readTimeout, err = time.ParseDuration(*readTimeoutStr)
  140. if err != nil {
  141. log.WithField("read_timeout", *readTimeoutStr).
  142. WithError(err).
  143. Fatal("read_timeout duration string invalid")
  144. }
  145. if readTimeout.Seconds() < 1 {
  146. log.WithField("read_timeout", *readTimeoutStr).
  147. Fatal("read_timeout less than one second")
  148. }
  149. writeTimeout, err = time.ParseDuration(*writeTimeoutStr)
  150. if err != nil {
  151. log.WithField("write_timeout", *writeTimeoutStr).
  152. WithError(err).
  153. Fatal("write_timeout duration string invalid")
  154. }
  155. if writeTimeout.Seconds() < 1 {
  156. log.WithField("write_timeout", *writeTimeoutStr).
  157. Fatal("write_timeout less than one second")
  158. }
  159. dataTimeout, err = time.ParseDuration(*dataTimeoutStr)
  160. if err != nil {
  161. log.WithField("data_timeout", *dataTimeoutStr).
  162. WithError(err).
  163. Fatal("data_timeout duration string invalid")
  164. }
  165. if dataTimeout.Seconds() < 1 {
  166. log.WithField("data_timeout", *dataTimeoutStr).
  167. Fatal("data_timeout less than one second")
  168. }
  169. }
  170. func ConfigLoad() {
  171. // use .env file if it exists
  172. if _, err := os.Stat(".env"); err == nil {
  173. if err := ff.Parse(flagset, os.Args[1:],
  174. ff.WithEnvVarPrefix("smtprelay"),
  175. ff.WithConfigFile(".env"),
  176. ff.WithConfigFileParser(ff.EnvParser),
  177. ); err != nil {
  178. fmt.Fprintf(os.Stderr, "error: %v\n", err)
  179. os.Exit(1)
  180. }
  181. } else {
  182. // use env variables and smtprelay.ini file
  183. if err := ff.Parse(flagset, os.Args[1:],
  184. ff.WithEnvVarPrefix("smtprelay"),
  185. ff.WithConfigFileFlag("config"),
  186. ff.WithConfigFileParser(IniParser),
  187. ); err != nil {
  188. fmt.Fprintf(os.Stderr, "error: %v\n", err)
  189. os.Exit(1)
  190. }
  191. }
  192. // Set up logging as soon as possible
  193. setupLogger()
  194. if *versionInfo {
  195. fmt.Printf("smtprelay/%s (%s)\n", appVersion, buildTime)
  196. os.Exit(0)
  197. }
  198. if *remotesStr == "" && *command == "" {
  199. log.Warn("no remotes or command set; mail will not be forwarded!")
  200. }
  201. setupAllowedNetworks()
  202. setupAllowedPatterns()
  203. setupRemotes()
  204. setupListeners()
  205. setupTimeouts()
  206. }
  207. // IniParser is a parser for config files in classic key/value style format. Each
  208. // line is tokenized as a single key/value pair. The first "=" delimited
  209. // token in the line is interpreted as the flag name, and all remaining tokens
  210. // are interpreted as the value. Any leading hyphens on the flag name are
  211. // ignored.
  212. func IniParser(r io.Reader, set func(name, value string) error) error {
  213. s := bufio.NewScanner(r)
  214. for s.Scan() {
  215. line := strings.TrimSpace(s.Text())
  216. if line == "" {
  217. continue // skip empties
  218. }
  219. if line[0] == '#' || line[0] == ';' {
  220. continue // skip comments
  221. }
  222. var (
  223. name string
  224. value string
  225. index = strings.IndexRune(line, '=')
  226. )
  227. if index < 0 {
  228. name, value = line, "true" // boolean option
  229. } else {
  230. name, value = strings.TrimSpace(line[:index]), strings.Trim(strings.TrimSpace(line[index+1:]), "\"")
  231. }
  232. if i := strings.Index(value, " #"); i >= 0 {
  233. value = strings.TrimSpace(value[:i])
  234. }
  235. if err := set(name, value); err != nil {
  236. return err
  237. }
  238. }
  239. return nil
  240. }