Browse Source

First release

master 1.0.0
LowEel 2 weeks ago
parent
commit
d1e88d2564
23 changed files with 1663 additions and 0 deletions
  1. +12
    -0
      .gitignore
  2. +14
    -0
      LICENSE
  3. +120
    -0
      README.md
  4. +71
    -0
      backend.go
  5. +8
    -0
      go.mod
  6. +6
    -0
      go.sum
  7. +30
    -0
      handler.go
  8. +3
    -0
      recipients.conf.example
  9. +19
    -0
      run.sh
  10. +72
    -0
      session.go
  11. +756
    -0
      smtpd/smtp.go
  12. +14
    -0
      vendor/github.com/amalfra/maildir/.gitignore
  13. +13
    -0
      vendor/github.com/amalfra/maildir/.travis.yml
  14. +21
    -0
      vendor/github.com/amalfra/maildir/LICENSE
  15. +12
    -0
      vendor/github.com/amalfra/maildir/Makefile
  16. +98
    -0
      vendor/github.com/amalfra/maildir/README.md
  17. +10
    -0
      vendor/github.com/amalfra/maildir/lib/consts.go
  18. +144
    -0
      vendor/github.com/amalfra/maildir/lib/message.go
  19. +46
    -0
      vendor/github.com/amalfra/maildir/lib/uniqueName.go
  20. +36
    -0
      vendor/github.com/amalfra/maildir/lib/utils.go
  21. +95
    -0
      vendor/github.com/amalfra/maildir/maildir.go
  22. +3
    -0
      vendor/modules.txt
  23. +60
    -0
      zangtumb.go

+ 12
- 0
.gitignore View File

@ -0,0 +1,12 @@
zangtumb
mail/*
certs/*
recipients.conf
binaries
binaries/*
build.sh
.vscode
.vscode/*
build.sh
binaries
binaries/*

+ 14
- 0
LICENSE View File

@ -0,0 +1,14 @@
Copyright (C) 2020 loweel@keinpfusch.net
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

+ 120
- 0
README.md View File

@ -0,0 +1,120 @@
Futuristic SMTP INBOUND-only server for home usage.
Inspired by Marinetti's RFC (AKA _Manifesto of Futurism_).
It only serves a precise list of email address. No aliases.
Everything else will be apparently accepted, and then discarded.
So that, spammers will waste their time (and money).
# REQUIREMENTS:
- Golang version >= 1.13
- git
# INSTALLATION
First download the code into the folder you want to use with Golang
```
git clone https://git.keinpfusch.net/loweel/zangtumb.git
go build -mod=vendor
./zangtumb to start the daemon , after setting the environment strings.
```
# CONFIGURATION
zangtumb is designed be easy to dockerize.
A reference pseudo-dockerfile could be:
```dockerfile
FROM debian:stable-slim
##MAIN
ENV KEYFILE "/certs/"mydomain.key"
ENV CERTFILE "/certs/mydomain.crt"
ENV DOMAINNAME "mydomain.tld"
ENV LISTEN ":5025"
##SESSION
ENV RECIPIENTS "recipients.conf"
ENV MAILFOLDER "/zangmail"
##MAIN
ENV USETLS="true"
## HERE WE GO
RUN useradd -ms /bin/bash zangtumb
RUN mkdir -p /opt/zangtumb
RUN mkdir -p /zangmail
COPY . /opt/zangtumb/
RUN chown -R zangtumb:zangtumb /opt/zangtumb
RUN chown -R zangtumb:zangtumb /zangmail
EXPOSE 5025
USER zangtumb
WORKDIR /opt/zangtumb
ENTRYPOINT ["/opt/zangtumb/zangtumb"]
```
everything is configured using ENV strings , as follows
| ENV STRING | Example value | Meaning |
| ---------- | --------------------- | ------------------------------------------------------------ |
| KEYFILE | "/certs/mydomain.key" | Path for private key. Only needed when using TLS. Which means, well... it's your email. So you don't want to send it in clear, isn't it? |
| CERTFILE | "/certs/mydomain.crt" | Path for certificate. Only needed when using TLS. Which means, well... it's your email. So you don't want to send it in clear, isn't it? |
| DOMAINNAME | "mydomain.tld" | will declare this value on the banner. No impact on recipients. |
| LISTEN | ":5025" | Address to listen in golang format. This example will listen to port 5025 on all interfaces. You may specify a specific interface like "1.2.3.4:5025" |
| RECIPIENTS | "recipients.conf" | File containing a list with email to serve. One mail address per line. Please notice, that pippo@pluto.com and pippo@paperino.com will end in the same mailbox, "pippo". |
| MAILFOLDER | "/zangmail" | Root of mailfolder. Mail is stored in the default dovecot Maildir format, meaning in the example "/zangmail/%u/Maildir" . |
| USETLS | "true" | Whether to force all to use TLS or not. yes. Do it. |
That's it.
# FAQ
- _This TLS behavior is violating RFC 2487_
- To give a shit of RFCs is a de facto standard. It works, and no spammer will ever buy a certificate per each spambot.
- _The minimal amount of recipients by RFC 5321 is 100. You reduced it._
- Yes. The reason is, we allow the ones we need. No more. This server is supposed to run inside a Raspberry, if needed. Call the RFC police, if you don't like.
- _The example dockerfile is way too big. Why no multistage?_
- This is because is an example. An example must be easy to understand. An example must be _simple_. Even you should be able to understand it. Well.... ok. Let's say, even _Bob_ should.
- _Why don't you use opensmtpd?_
- To make this server took less than dockerizing opensmtpd in a decent way.
- _Why don't you use postfix/sendmail/qmail/courier_
- I serve 4 mailboxes in total. Why should I deploy all that complexity? Complexity != security.
- _Silently discarrding email after pretending you've accepted is not nice. Perhaps, this will make your server to look like an open relay._
- Unfortunately, English cannot translate the correct answer, which is "esticazzi non ce lo scriviamo?". So I can't properly answer you.
- _This golang code is not idiomatic. And there is no graphene, no quantum computing, no UI/UX and no horizontal scaling of Internet of Things with Artificial Intelligence of Big Data._
- Please, bring me a Frappuccino.

+ 71
- 0
backend.go View File

@ -0,0 +1,71 @@
package main
import (
"bufio"
"log"
"os"
"strings"
)
// The Backend implements SMTP server methods.
type Backend struct {
MailBaseFolder string
ValidRecipientsFile string
ValidRecipientsMap map[string]struct{}
MaxRecipients int
}
var SmtpBackend *Backend
// Load Valid Recipients
func (bkd *Backend) LoadValidRecipients() error {
bkd.MaxRecipients = 0
var void struct{}
file, err := os.Open(bkd.ValidRecipientsFile)
if err != nil {
log.Println(err)
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
s := strings.Trim(scanner.Text(), " ")
bkd.ValidRecipientsMap[s] = void
log.Printf("RCPT: <%s>", s)
bkd.MaxRecipients++
}
return nil
}
// Checks if Mail is a valid recipient
func (bkd *Backend) CheckValidRcpt(to string) bool {
_, isValid := bkd.ValidRecipientsMap[to]
return isValid
}
func init() {
SmtpBackend = new(Backend)
SmtpBackend.ValidRecipientsMap = make(map[string]struct{})
SmtpBackend.ValidRecipientsFile = os.Getenv("RECIPIENTS")
if SmtpBackend.ValidRecipientsFile == "" {
SmtpBackend.ValidRecipientsFile = "./recipients.conf"
}
SmtpBackend.MailBaseFolder = os.Getenv("MAILFOLDER")
if SmtpBackend.MailBaseFolder == "" {
SmtpBackend.MailBaseFolder = "./mail"
}
if err := SmtpBackend.LoadValidRecipients(); err != nil {
log.Println("Failed to load Recipients", err)
} else {
log.Println("Recipients Loaded")
}
}

+ 8
- 0
go.mod View File

@ -0,0 +1,8 @@
module zangtumb
go 1.13
require (
github.com/amalfra/maildir v0.0.7
github.com/emersion/go-smtp v0.12.1 // indirect
)

+ 6
- 0
go.sum View File

@ -0,0 +1,6 @@
github.com/amalfra/maildir v0.0.7 h1:quK6vVGmjqP1zbeo9g7jtx37htGoYrKKlSpcDcplD04=
github.com/amalfra/maildir v0.0.7/go.mod h1:+ynYowCpUHTWebUhg3Sb6Jp2dq+SUSWLTSLIf7Vy+ak=
github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e h1:ba7YsgX5OV8FjGi5ZWml8Jng6oBrJAb3ahqWMJ5Ce8Q=
github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-smtp v0.12.1 h1:1R8BDqrR2HhlGwgFYcOi+BVTvK1bMjAB65QcVpJ5sNA=
github.com/emersion/go-smtp v0.12.1/go.mod h1:SD9V/xa4ndMw77lR3Mf7htkp8RBNYuPh9UeuBs9tpUQ=

+ 30
- 0
handler.go View File

@ -0,0 +1,30 @@
package main
import (
"bytes"
"log"
"net"
)
func mailHandler(origin net.Addr, from string, to []string, data []byte) {
SmtpSession := new(Session)
log.Printf("Received mail from %s for %q from %s", from, to, origin.String())
log.Println(" Recipients: ", to)
for _, rcptTo := range to {
SmtpSession.Reset()
if rcptErr := SmtpSession.Rcpt(rcptTo); rcptErr == nil {
SmtpSession.MailFrom = from
if dataErr := SmtpSession.Data(bytes.NewReader(data)); dataErr != nil {
log.Println("Problem Saving Message: ", dataErr.Error())
}
log.Println("Session is: ", SmtpSession)
} else {
log.Println(rcptErr)
}
}
}

+ 3
- 0
recipients.conf.example View File

@ -0,0 +1,3 @@
joe@example.org
alice@whatever.com
bob@subgenius.org

+ 19
- 0
run.sh View File

@ -0,0 +1,19 @@
#!/bin/bash
##MAIN
export KEYFILE="certs/server.key"
export CERTFILE="certs/server.crt"
export DOMAINNAME="testDomain"
export LISTEN=":1025"
##SESSION
export RECIPIENTS="./recipients.conf"
export MAILFOLDER="mail"
##MAIN
export USETLS="false"
##RUN
./zangtumb

+ 72
- 0
session.go View File

@ -0,0 +1,72 @@
package main
import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"strings"
"time"
"github.com/amalfra/maildir"
)
// A Session is returned after successful login.
type Session struct {
MailFrom string
RcptTo string
LocalUser string
MailDomain string
Valid bool
}
func (s *Session) Rcpt(to string) error {
log.Println("Rcpt to:", to)
if !SmtpBackend.CheckValidRcpt(to) {
time.Sleep(10 * time.Second)
return errors.New("YU NO MAIL!!! WE NU RELAY")
}
s.RcptTo = to
if strings.Contains(to, "@") {
rcptArray := strings.Split(to, "@")
s.MailDomain = rcptArray[1]
s.LocalUser = rcptArray[0]
s.Valid = true
} else {
s.Valid = false
}
return nil
}
func (s *Session) Data(r io.Reader) error {
log.Println("Start Saving Message")
if !s.Valid {
log.Println("Message Was Not Valid: ", s)
return nil
}
if b, err := ioutil.ReadAll(r); err != nil {
return err
} else {
//using the default Maildir structure
mad := fmt.Sprintf("%s/%s/Maildir", SmtpBackend.MailBaseFolder, s.LocalUser)
log.Println("Maildir is:", mad)
myMaildir := maildir.NewMaildir(mad)
_, errMd := myMaildir.Add(string(b))
if errMd != nil {
log.Println("Error Saving mail ")
return errMd
}
}
return nil
}
func (s *Session) Reset() {
s.LocalUser = ""
s.MailDomain = ""
s.MailFrom = ""
s.RcptTo = ""
s.Valid = false
}

+ 756
- 0
smtpd/smtp.go View File

@ -0,0 +1,756 @@
// Package smtpd implements a basic SMTP server.
// https://github.com/mhale/smtpd
package smtpd
import (
"bufio"
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"regexp"
"strconv"
"strings"
"time"
)
var (
// Debug `true` enables verbose logging.
Debug = false
rcptToRE = regexp.MustCompile(`[Tt][Oo]:[ ]*<(.+[@].+)>`) // We don't receive for local users with no domain.
mailFromRE = regexp.MustCompile(`[Ff][Rr][Oo][Mm]:[ ]*<(.*)>(\s(.*))?`) // Delivery Status Notifications are sent with "MAIL FROM:<>"
mailSizeRE = regexp.MustCompile(`[Ss][Ii][Zz][Ee]=(\d+)`)
)
// Handler function called upon successful receipt of an email.
type Handler func(remoteAddr net.Addr, from string, to []string, data []byte)
// HandlerRcpt function called on RCPT. Return accept status.
type HandlerRcpt func(remoteAddr net.Addr, from string, to string) bool
// AuthHandler function called when a login attempt is performed. Returns true if credentials are correct.
type AuthHandler func(remoteAddr net.Addr, mechanism string, username []byte, password []byte, shared []byte) (bool, error)
// ListenAndServe listens on the TCP network address addr
// and then calls Serve with handler to handle requests
// on incoming connections.
func ListenAndServe(addr string, handler Handler, appname string, hostname string, instance *Server) error {
srv := instance
srv.Addr = addr
srv.Handler = handler
srv.Appname = appname
srv.Hostname = hostname
return srv.ListenAndServe()
}
type maxSizeExceededError struct {
limit int
}
func maxSizeExceeded(limit int) maxSizeExceededError {
return maxSizeExceededError{limit}
}
// Error uses the RFC 5321 response message in preference to RFC 1870.
// RFC 3463 defines enhanced status code x.3.4 as "Message too big for system".
func (err maxSizeExceededError) Error() string {
return fmt.Sprintf("552 5.3.4 Requested mail action aborted: exceeded storage allocation (%d)", err.limit)
}
// LogFunc is a function capable of logging the client-server communication.
type LogFunc func(remoteIP, verb, line string)
// Server is an SMTP server.
type Server struct {
Addr string // TCP address to listen on, defaults to ":25" (all addresses, port 25) if empty
Appname string
AuthHandler AuthHandler
AuthMechs map[string]bool // Override list of allowed authentication mechanisms. Currently supported: LOGIN, PLAIN, CRAM-MD5. Enabling LOGIN and PLAIN will reduce RFC 4954 compliance.
AuthRequired bool // Require authentication for every command except AUTH, EHLO, HELO, NOOP, RSET or QUIT as per RFC 4954. Ignored if AuthHandler is not configured.
Handler Handler
HandlerRcpt HandlerRcpt
Hostname string
LogRead LogFunc
LogWrite LogFunc
MaxSize int // Maximum message size allowed, in bytes
MaxRcpt int // Maximum number of recipients
Timeout time.Duration
TLSConfig *tls.Config
TLSListener bool // Listen for incoming TLS connections only (not recommended as it may reduce compatibility). Ignored if TLS is not configured.
TLSRequired bool // Require TLS for every command except NOOP, EHLO, STARTTLS, or QUIT as per RFC 3207. Ignored if TLS is not configured.
}
// ConfigureTLS creates a TLS configuration from certificate and key files.
func (srv *Server) ConfigureTLS(certFile string, keyFile string) error {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return err
}
srv.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
return nil
}
// ConfigureTLSWithPassphrase creates a TLS configuration from a certificate,
// an encrypted key file and the associated passphrase:
func (srv *Server) ConfigureTLSWithPassphrase(
certFile string,
keyFile string,
passphrase string,
) error {
certPEMBlock, err := ioutil.ReadFile(certFile)
if err != nil {
return err
}
keyPEMBlock, err := ioutil.ReadFile(keyFile)
if err != nil {
return err
}
keyDERBlock, _ := pem.Decode(keyPEMBlock)
keyPEMDecrypted, err := x509.DecryptPEMBlock(keyDERBlock, []byte(passphrase))
if err != nil {
return err
}
var pemBlock pem.Block
pemBlock.Type = keyDERBlock.Type
pemBlock.Bytes = keyPEMDecrypted
keyPEMBlock = pem.EncodeToMemory(&pemBlock)
cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
return err
}
srv.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
return nil
}
// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections. If
// srv.Addr is blank, ":25" is used.
func (srv *Server) ListenAndServe() error {
if srv.Addr == "" {
srv.Addr = ":25"
}
if srv.Appname == "" {
srv.Appname = "smtpd"
}
if srv.Hostname == "" {
srv.Hostname, _ = os.Hostname()
}
if srv.Timeout == 0 {
srv.Timeout = 5 * time.Minute
}
var ln net.Listener
var err error
// If TLSListener is enabled, listen for TLS connections only.
if srv.TLSConfig != nil && srv.TLSListener {
ln, err = tls.Listen("tcp", srv.Addr, srv.TLSConfig)
} else {
ln, err = net.Listen("tcp", srv.Addr)
}
if err != nil {
return err
}
return srv.Serve(ln)
}
// Serve creates a new SMTP session after a network connection is established.
func (srv *Server) Serve(ln net.Listener) error {
defer ln.Close()
for {
conn, err := ln.Accept()
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
continue
}
return err
}
session := srv.newSession(conn)
go session.serve()
}
}
type session struct {
srv *Server
conn net.Conn
br *bufio.Reader
bw *bufio.Writer
remoteIP string // Remote IP address
remoteHost string // Remote hostname according to reverse DNS lookup
remoteName string // Remote hostname as supplied with EHLO
tls bool
authenticated bool
}
// Create new session from connection.
func (srv *Server) newSession(conn net.Conn) (s *session) {
s = &session{
srv: srv,
conn: conn,
br: bufio.NewReader(conn),
bw: bufio.NewWriter(conn),
}
// Get remote end info for the Received header.
s.remoteIP, _, _ = net.SplitHostPort(s.conn.RemoteAddr().String())
names, err := net.LookupAddr(s.remoteIP)
if err == nil && len(names) > 0 {
s.remoteHost = names[0]
} else {
s.remoteHost = "unknown"
}
// Set tls = true if TLS is already in use.
_, s.tls = s.conn.(*tls.Conn)
return
}
// Function called to handle connection requests.
func (s *session) serve() {
defer s.conn.Close()
var from string
var gotFrom bool
var to []string
var buffer bytes.Buffer
// Send banner.
s.writef("220 %s %s ESMTP Service ready", s.srv.Hostname, s.srv.Appname)
loop:
for {
// Attempt to read a line from the socket.
// On timeout, send a timeout message and return from serve().
// On error, assume the client has gone away i.e. return from serve().
line, err := s.readLine()
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
s.writef("421 4.4.2 %s %s ESMTP Service closing transmission channel after timeout exceeded", s.srv.Hostname, s.srv.Appname)
}
break
}
verb, args := s.parseLine(line)
switch verb {
case "HELO":
s.remoteName = args
s.writef("250 %s greets %s", s.srv.Hostname, s.remoteName)
// RFC 2821 section 4.1.4 specifies that EHLO has the same effect as RSET, so reset for HELO too.
from = ""
gotFrom = false
to = nil
buffer.Reset()
case "EHLO":
s.remoteName = args
s.writef(s.makeEHLOResponse())
// RFC 2821 section 4.1.4 specifies that EHLO has the same effect as RSET.
from = ""
gotFrom = false
to = nil
buffer.Reset()
case "MAIL":
if s.srv.TLSConfig != nil && s.srv.TLSRequired && !s.tls {
s.writef("530 5.7.0 Must issue a STARTTLS command first")
break
}
if s.srv.AuthHandler != nil && s.srv.AuthRequired && !s.authenticated {
s.writef("530 5.7.0 Authentication required")
break
}
match := mailFromRE.FindStringSubmatch(args)
if match == nil {
s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid FROM parameter)")
} else {
// Validate the SIZE parameter if one was sent.
if len(match[2]) > 0 { // A parameter is present
sizeMatch := mailSizeRE.FindStringSubmatch(match[3])
if sizeMatch == nil {
s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid SIZE parameter)")
} else {
// Enforce the maximum message size if one is set.
size, err := strconv.Atoi(sizeMatch[1])
if err != nil { // Bad SIZE parameter
s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid SIZE parameter)")
} else if s.srv.MaxSize > 0 && size > s.srv.MaxSize { // SIZE above maximum size, if set
err = maxSizeExceeded(s.srv.MaxSize)
s.writef(err.Error())
} else { // SIZE ok
from = match[1]
gotFrom = true
s.writef("250 2.1.0 Ok")
}
}
} else { // No parameters after FROM
from = match[1]
gotFrom = true
s.writef("250 2.1.0 Ok")
}
}
to = nil
buffer.Reset()
case "RCPT":
if s.srv.TLSConfig != nil && s.srv.TLSRequired && !s.tls {
s.writef("530 5.7.0 Must issue a STARTTLS command first")
break
}
if s.srv.AuthHandler != nil && s.srv.AuthRequired && !s.authenticated {
s.writef("530 5.7.0 Authentication required")
break
}
if !gotFrom {
s.writef("503 5.5.1 Bad sequence of commands (MAIL required before RCPT)")
break
}
match := rcptToRE.FindStringSubmatch(args)
if match == nil {
s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid TO parameter)")
} else {
// RFC 5321 specifies 100 minimum recipients
// but I give a shit of RFC5321
if len(to) == s.srv.MaxRcpt {
s.writef("452 4.5.3 Too many recipients, %d > %d ", len(to), s.srv.MaxRcpt)
} else {
accept := true
if s.srv.HandlerRcpt != nil {
accept = s.srv.HandlerRcpt(s.conn.RemoteAddr(), from, match[1])
}
if accept {
to = append(to, match[1])
s.writef("250 2.1.5 Ok")
} else {
s.writef("550 5.1.0 Requested action not taken: mailbox unavailable")
}
}
}
case "DATA":
if s.srv.TLSConfig != nil && s.srv.TLSRequired && !s.tls {
s.writef("530 5.7.0 Must issue a STARTTLS command first")
break
}
if s.srv.AuthHandler != nil && s.srv.AuthRequired && !s.authenticated {
s.writef("530 5.7.0 Authentication required")
break
}
if !gotFrom || len(to) == 0 {
s.writef("503 5.5.1 Bad sequence of commands (MAIL & RCPT required before DATA)")
break
}
s.writef("354 Start mail input; end with <CR><LF>.<CR><LF>")
// Attempt to read message body from the socket.
// On timeout, send a timeout message and return from serve().
// On net.Error, assume the client has gone away i.e. return from serve().
// On other errors, allow the client to try again.
data, err := s.readData()
if err != nil {
switch err.(type) {
case net.Error:
if err.(net.Error).Timeout() {
s.writef("421 4.4.2 %s %s ESMTP Service closing transmission channel after timeout exceeded", s.srv.Hostname, s.srv.Appname)
}
break loop
case maxSizeExceededError:
s.writef(err.Error())
continue
default:
s.writef("451 4.3.0 Requested action aborted: local error in processing")
continue
}
}
// Create Received header & write message body into buffer.
buffer.Reset()
buffer.Write(s.makeHeaders(to))
buffer.Write(data)
s.writef("250 2.0.0 Ok: queued")
// Pass mail on to handler.
if s.srv.Handler != nil {
go s.srv.Handler(s.conn.RemoteAddr(), from, to, buffer.Bytes())
}
// Reset for next mail.
from = ""
gotFrom = false
to = nil
buffer.Reset()
case "QUIT":
s.writef("221 2.0.0 %s %s ESMTP Service closing transmission channel", s.srv.Hostname, s.srv.Appname)
break loop
case "RSET":
if s.srv.TLSConfig != nil && s.srv.TLSRequired && !s.tls {
s.writef("530 5.7.0 Must issue a STARTTLS command first")
break
}
s.writef("250 2.0.0 Ok")
from = ""
gotFrom = false
to = nil
buffer.Reset()
case "NOOP":
s.writef("250 2.0.0 Ok")
case "HELP", "VRFY", "EXPN":
// See RFC 5321 section 4.2.4 for usage of 500 & 502 response codes.
s.writef("502 5.5.1 Command not implemented")
case "STARTTLS":
// Parameters are not allowed (RFC 3207 section 4).
if args != "" {
s.writef("501 5.5.2 Syntax error (no parameters allowed)")
break
}
// Handle case where TLS is requested but not configured (and therefore not listed as a service extension).
if s.srv.TLSConfig == nil {
s.writef("502 5.5.1 Command not implemented")
break
}
// Handle case where STARTTLS is received when TLS is already in use.
if s.tls {
s.writef("503 5.5.1 Bad sequence of commands (TLS already in use)")
break
}
s.writef("220 2.0.0 Ready to start TLS")
// Establish a TLS connection with the client.
tlsConn := tls.Server(s.conn, s.srv.TLSConfig)
err := tlsConn.Handshake()
if err != nil {
s.writef("403 4.7.0 TLS handshake failed")
break
}
// TLS handshake succeeded, switch to using the TLS connection.
s.conn = tlsConn
s.br = bufio.NewReader(s.conn)
s.bw = bufio.NewWriter(s.conn)
s.tls = true
// RFC 3207 specifies that the server must discard any prior knowledge obtained from the client.
s.remoteName = ""
from = ""
gotFrom = false
to = nil
buffer.Reset()
case "AUTH":
if s.srv.TLSConfig != nil && s.srv.TLSRequired && !s.tls {
s.writef("530 5.7.0 Must issue a STARTTLS command first")
break
}
// Handle case where AUTH is requested but not configured (and therefore not listed as a service extension).
if s.srv.AuthHandler == nil {
s.writef("502 5.5.1 Command not implemented")
break
}
// Handle case where AUTH is received when already authenticated.
if s.authenticated {
s.writef("503 5.5.1 Bad sequence of commands (already authenticated for this session)")
break
}
// RFC 4954 specifies that AUTH is not permitted during mail transactions.
if gotFrom || len(to) > 0 {
s.writef("503 5.5.1 Bad sequence of commands (AUTH not permitted during mail transaction)")
break
}
// RFC 4954 requires a mechanism parameter.
authType, authArgs := s.parseLine(args)
if authType == "" {
s.writef("501 5.5.4 Malformed AUTH input (argument required)")
break
}
// RFC 4954 requires rejecting unsupported authentication mechanisms with a 504 response.
allowedAuth := s.authMechs()
if allowed, found := allowedAuth[authType]; !found || !allowed {
s.writef("504 5.5.4 Unrecognized authentication type")
break
}
// RFC 4954 also specifies that ESMTP code 5.5.4 ("Invalid command arguments") should be returned
// when attempting to use an unsupported authentication type.
// Many servers return 5.7.4 ("Security features not supported") instead.
switch authType {
case "PLAIN":
s.authenticated, err = s.handleAuthPlain(authArgs)
case "LOGIN":
s.authenticated, err = s.handleAuthLogin(authArgs)
case "CRAM-MD5":
s.authenticated, err = s.handleAuthCramMD5()
}
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
s.writef("421 4.4.2 %s %s ESMTP Service closing transmission channel after timeout exceeded", s.srv.Hostname, s.srv.Appname)
break loop
}
s.writef(err.Error())
break
}
if s.authenticated {
s.writef("235 2.7.0 Authentication successful")
} else {
s.writef("535 5.7.8 Authentication credentials invalid")
}
default:
// See RFC 5321 section 4.2.4 for usage of 500 & 502 response codes.
s.writef("500 5.5.2 Syntax error, command unrecognized")
}
}
}
// Wrapper function for writing a complete line to the socket.
func (s *session) writef(format string, args ...interface{}) error {
if s.srv.Timeout > 0 {
s.conn.SetWriteDeadline(time.Now().Add(s.srv.Timeout))
}
line := fmt.Sprintf(format, args...)
fmt.Fprintf(s.bw, line+"\r\n")
err := s.bw.Flush()
if Debug {
verb := "WROTE"
if s.srv.LogWrite != nil {
s.srv.LogWrite(s.remoteIP, verb, line)
} else {
log.Println(s.remoteIP, verb, line)
}
}
return err
}
// Read a complete line from the socket.
func (s *session) readLine() (string, error) {
if s.srv.Timeout > 0 {
s.conn.SetReadDeadline(time.Now().Add(s.srv.Timeout))
}
line, err := s.br.ReadString('\n')
if err != nil {
return "", err
}
line = strings.TrimSpace(line) // Strip trailing \r\n
if Debug {
verb := "READ"
if s.srv.LogRead != nil {
s.srv.LogRead(s.remoteIP, verb, line)
} else {
log.Println(s.remoteIP, verb, line)
}
}
return line, err
}
// Parse a line read from the socket.
func (s *session) parseLine(line string) (verb string, args string) {
if idx := strings.Index(line, " "); idx != -1 {
verb = strings.ToUpper(line[:idx])
args = strings.TrimSpace(line[idx+1:])
} else {
verb = strings.ToUpper(line)
args = ""
}
return verb, args
}
// Read the message data following a DATA command.
func (s *session) readData() ([]byte, error) {
var data []byte
for {
if s.srv.Timeout > 0 {
s.conn.SetReadDeadline(time.Now().Add(s.srv.Timeout))
}
line, err := s.br.ReadBytes('\n')
if err != nil {
return nil, err
}
// Handle end of data denoted by lone period (\r\n.\r\n)
if bytes.Equal(line, []byte(".\r\n")) {
break
}
// Remove leading period (RFC 5321 section 4.5.2)
if line[0] == '.' {
line = line[1:]
}
// Enforce the maximum message size limit.
if s.srv.MaxSize > 0 {
if len(data)+len(line) > s.srv.MaxSize {
_, _ = s.br.Discard(s.br.Buffered()) // Discard the buffer remnants.
return nil, maxSizeExceeded(s.srv.MaxSize)
}
}
data = append(data, line...)
}
return data, nil
}
// Create the Received header to comply with RFC 2821 section 3.8.2.
// TODO: Work out what to do with multiple to addresses.
func (s *session) makeHeaders(to []string) []byte {
var buffer bytes.Buffer
now := time.Now().Format("Mon, _2 Jan 2006 15:04:05 -0700 (MST)")
buffer.WriteString(fmt.Sprintf("Received: from %s (%s [%s])\r\n", s.remoteName, s.remoteHost, s.remoteIP))
buffer.WriteString(fmt.Sprintf(" by %s (%s) with SMTP\r\n", s.srv.Hostname, s.srv.Appname))
buffer.WriteString(fmt.Sprintf(" for <%s>; %s\r\n", to[0], now))
return buffer.Bytes()
}
// Determine allowed authentication mechanisms.
// RFC 4954 specifies that plaintext authentication mechanisms such as LOGIN and PLAIN require a TLS connection.
// This can be explicitly overridden e.g. setting s.srv.AuthMechs["LOGIN"] = true.
func (s *session) authMechs() (mechs map[string]bool) {
mechs = map[string]bool{"LOGIN": s.tls, "PLAIN": s.tls, "CRAM-MD5": true}
for mech := range mechs {
allowed, found := s.srv.AuthMechs[mech]
if found {
mechs[mech] = allowed
}
}
return
}
// Create the greeting string sent in response to an EHLO command.
func (s *session) makeEHLOResponse() (response string) {
response = fmt.Sprintf("250-%s greets %s\r\n", s.srv.Hostname, s.remoteName)
// RFC 1870 specifies that "SIZE 0" indicates no maximum size is in force.
response += fmt.Sprintf("250-SIZE %d\r\n", s.srv.MaxSize)
// Only list STARTTLS if TLS is configured, but not currently in use.
if s.srv.TLSConfig != nil && !s.tls {
response += "250-STARTTLS\r\n"
}
// Only list AUTH if an AuthHandler is configured and at least one mechanism is allowed.
if s.srv.AuthHandler != nil {
var mechs []string
for mech, allowed := range s.authMechs() {
if allowed {
mechs = append(mechs, mech)
}
}
if len(mechs) > 0 {
response += "250-AUTH " + strings.Join(mechs, " ") + "\r\n"
}
}
response += "250 ENHANCEDSTATUSCODES"
return
}
func (s *session) handleAuthLogin(arg string) (bool, error) {
var err error
if arg == "" {
s.writef("334 " + base64.StdEncoding.EncodeToString([]byte("Username:")))
arg, err = s.readLine()
if err != nil {
return false, err
}
}
username, err := base64.StdEncoding.DecodeString(arg)
if err != nil {
return false, errors.New("501 5.5.2 Syntax error (unable to decode)")
}
s.writef("334 " + base64.StdEncoding.EncodeToString([]byte("Password:")))
line, err := s.readLine()
if err != nil {
return false, err
}
password, err := base64.StdEncoding.DecodeString(line)
if err != nil {
return false, errors.New("501 5.5.2 Syntax error (unable to decode)")
}
// Validate credentials.
authenticated, err := s.srv.AuthHandler(s.conn.RemoteAddr(), "LOGIN", username, password, nil)
return authenticated, err
}
func (s *session) handleAuthPlain(arg string) (bool, error) {
var err error
// If fast mode (AUTH PLAIN [arg]) is not used, prompt for credentials.
if arg == "" {
s.writef("334 ")
arg, err = s.readLine()
if err != nil {
return false, err
}
}
data, err := base64.StdEncoding.DecodeString(arg)
if err != nil {
return false, errors.New("501 5.5.2 Syntax error (unable to decode)")
}
parts := bytes.Split(data, []byte{0})
if len(parts) != 3 {
return false, errors.New("501 5.5.2 Syntax error (unable to parse)")
}
// Validate credentials.
authenticated, err := s.srv.AuthHandler(s.conn.RemoteAddr(), "PLAIN", parts[1], parts[2], nil)
return authenticated, err
}
func (s *session) handleAuthCramMD5() (bool, error) {
shared := "<" + strconv.Itoa(os.Getpid()) + "." + strconv.Itoa(time.Now().Nanosecond()) + "@" + s.srv.Hostname + ">"
s.writef("334 " + base64.StdEncoding.EncodeToString([]byte(shared)))
data, err := s.readLine()
if err != nil {
return false, err
}
if data == "*" {
return false, errors.New("501 5.7.0 Authentication cancelled")
}
buf, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return false, errors.New("501 5.5.2 Syntax error (unable to decode)")
}
fields := strings.Split(string(buf), " ")
if len(fields) < 2 {
return false, errors.New("501 5.5.2 Syntax error (unable to parse)")
}
// Validate credentials.
authenticated, err := s.srv.AuthHandler(s.conn.RemoteAddr(), "CRAM-MD5", []byte(fields[0]), []byte(fields[1]), []byte(shared))
return authenticated, err
}

+ 14
- 0
vendor/github.com/amalfra/maildir/.gitignore View File

@ -0,0 +1,14 @@
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/

+ 13
- 0
vendor/github.com/amalfra/maildir/.travis.yml View File

@ -0,0 +1,13 @@
os:
- linux
- osx
language: go
go:
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
script: make build

+ 21
- 0
vendor/github.com/amalfra/maildir/LICENSE View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Amal Francis
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 permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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
- 0
vendor/github.com/amalfra/maildir/Makefile View File

@ -0,0 +1,12 @@
.PHONY: all
fmt:
go fmt ./...
vet:
go vet ./...
test:
go test ./... -v
build: fmt vet test

+ 98
- 0
vendor/github.com/amalfra/maildir/README.md View File

@ -0,0 +1,98 @@
maildir
=======
[![GitHub release](https://img.shields.io/github/release/amalfra/maildir.svg)](https://github.com/amalfra/maildir/releases)
[![Build Status](https://travis-ci.org/amalfra/maildir.svg?branch=master)](https://travis-ci.org/amalfra/maildir)
[![GoDoc](https://godoc.org/github.com/amalfra/maildir?status.svg)](https://godoc.org/github.com/amalfra/maildir)
[![Go Report Card](https://goreportcard.com/badge/github.com/amalfra/maildir)](https://goreportcard.com/report/github.com/amalfra/maildir)
A go package for reading and writing messages in the maildir format.
> The Maildir e-mail format is a common way of storing e-mail messages, where each message is kept in a separate file with a unique name, and each folder is a directory. The local filesystem handles file locking as messages are added, moved and deleted. A major design goal of Maildir is to eliminate program code having to handle locking, which is often difficult.
Refer http://cr.yp.to/proto/maildir.html and http://en.wikipedia.org/wiki/Maildir
## Installation
You can download the package using
``` go
go get github.com/amalfra/maildir
```
## Usage
Next, import the package
``` go
import (
"github.com/amalfra/maildir"
)
```
#### Create a maildir in /home/amal/mail
``` go
myMaildir := maildir.NewMaildir("/home/amal/mail")
```
This command automatically creates the standard Maildir directories - `cur`,
`new`, and `tmp` - if they do not exist.
#### Add a new message
This creates a new file with the contents "foo"; returns the Message struct reference. Messages are written to the tmp dir then moved to new.
``` go
message, err := myMaildir.Add("foo")
```
#### List new messages
``` go
mailList, err := myMaildir.List("new")
```
This will return a map of messages by key, sorted by key
#### List current messages
``` go
mailList, err := myMaildir.List("cur")
```
This will return a map of messages by key, sorted by key
#### Find the message using key
``` go
message := maildir.Get(key)
```
#### Delete the message from disk by key
``` go
err := maildir.Delete(key)
```
**Below are the methods that are available on Message instance**
#### Get the key used to uniquely identify the message
``` go
key := message.Key()
```
#### Load the message content from file
``` go
data, err := message.GetData()
```
#### Process message - move the message from "new" to "cur"
This is usaully done to indicate that some process has retrieved the message.
``` go
key, err := message.Process("new")
```
## Development
Questions, problems or suggestions? Please post them on the [issue tracker](https://github.com/amalfra/maildir/issues).
You can contribute changes by forking the project and submitting a pull request. You can ensure the tests are passing by running ```make test```. Feel free to contribute :heart_eyes:
## UNDER MIT LICENSE
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 permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.

+ 10
- 0
vendor/github.com/amalfra/maildir/lib/consts.go View File

@ -0,0 +1,10 @@
package lib
// the seperator between unique name and info
const colon = ':'
// default info, to which flags are appended
const info = "2,"
// Subdirs has subdirectories that are required in maildir
var Subdirs = []string{"tmp", "new", "cur"}

+ 144
- 0
vendor/github.com/amalfra/maildir/lib/message.go View File

@ -0,0 +1,144 @@
package lib
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
// Message represents a maildir message and has it's supported operations
type Message struct {
dir string
maildir string
info string
unqiueName string
oldKey string
}
// NewMessage will create new message in specified mail directory
func NewMessage(maildir string) (*Message, error) {
var err error
msg := new(Message)
msg.maildir = maildir
msg.dir = "tmp"
msg.unqiueName, err = generate()
if err != nil {
return nil, errors.New("Failed to generate unqiue name")
}
return msg, nil
}
// parseKey will set dir, unqiueName, info based on the key
func (m *Message) parseKey(key string) {
// remove leading /
key = strings.TrimPrefix(key, string(os.PathSeparator))
parts := strings.Split(key, string(os.PathSeparator))
m.dir = parts[0]
filename := parts[1]
parts = strings.Split(filename, string(colon))
m.unqiueName = parts[0]
if len(parts) > 1 {
m.info = parts[1]
}
}
// LoadMessage will populate message object by loading info from passed key
func LoadMessage(maildir string, key string) *Message {
msg := new(Message)
msg.maildir = maildir
msg.parseKey(key)
return msg
}
// filename returns the filename of the message
func (m *Message) filename() string {
return fmt.Sprintf("%s%c%s", m.unqiueName, colon, m.info)
}
// Key returns the key to identify the message
func (m *Message) Key() string {
return filepath.Join(m.dir, m.filename())
}
// path returns the full path to the message
func (m *Message) path() string {
return filepath.Join(m.maildir, m.Key())
}
// oldPath returns the old full path to the message
func (m *Message) oldPath() string {
return filepath.Join(m.maildir, m.oldKey)
}
// rename the message. Returns the new key if successful
func (m *Message) rename(newDir string, newInfo string) (string, error) {
// Save the old key so we can revert to the old state
m.oldKey = m.Key()
// Set the new state
m.dir = newDir
if newInfo != "" {
m.info = newInfo
}
if m.oldPath() != m.path() {
err := os.Rename(m.oldPath(), m.path())
if err != nil {
// restore old state
if m.oldKey != "" {
m.parseKey(m.oldKey)
}
return "", errors.New("Failed to rename folder")
}
}
m.oldKey = ""
return m.Key(), nil
}
// Write will write data to disk. only work with messages which haven't been written to disk.
// After successfully writing to disk, rename the message to new dir
func (m *Message) Write(data string) error {
if m.dir != "tmp" {
return errors.New("Can only write messages in tmp")
}
err := ioutil.WriteFile(m.path(), []byte(data), os.ModePerm)
if err != nil {
return fmt.Errorf("Failed to write message to path %s", m.path())
}
_, err = m.rename("new", "")
if err != nil {
return errors.New("Failed to rename folder")
}
return nil
}
// Process will move a message from new to cur, add info. Returns the message's key
func (m *Message) Process() (string, error) {
return m.rename("cur", info)
}
// SetInfo will set info on a message
func (m *Message) SetInfo(infoStr string) (string, error) {
if m.dir != "cur" {
return "", errors.New("Can only set info on cur messages")
}
return m.rename("cur", infoStr)
}
// GetData returns the message's data from disk
func (m *Message) GetData() (string, error) {
dat, err := ioutil.ReadFile(m.path())
strDat := string(dat)
return strDat, err
}
// Destroy will remove the message file
func (m *Message) Destroy() error {
return os.Remove(m.path())
}

+ 46
- 0
vendor/github.com/amalfra/maildir/lib/uniqueName.go View File

@ -0,0 +1,46 @@
package lib
import (
"errors"
"fmt"
"os"
"time"
)
type uniqueName struct {
now time.Time
}
// generate will create and return unique file names for new messages
func generate() (string, error) {
uni := &uniqueName{now: time.Now()}
hostname, err := uni.right()
if err != nil {
return "", errors.New("Failed to fetch hostname")
}
return fmt.Sprintf("%d.%s.%s", uni.left(), uni.middle(), hostname), nil
}
// left part of the unique name is the number of seconds since the UNIX epoch
func (uni *uniqueName) left() int64 {
return uni.now.Unix()
}
func (uni *uniqueName) microseconds() int64 {
return uni.now.Unix() * 1000000
}
// middle part of the unique name contains microsecond, process id, and a per-process incrementing counter
func (uni *uniqueName) middle() string {
return fmt.Sprintf("M%06dP%dQ%d", uni.microseconds(), os.Getpid(), getCounter())
}
// right part is the hostname
func (uni *uniqueName) right() (string, error) {
name, err := os.Hostname()
if err != nil {
return "", err
}
return name, nil
}

+ 36
- 0
vendor/github.com/amalfra/maildir/lib/utils.go View File

@ -0,0 +1,36 @@
package lib
import (
"sync"
)
var counter int
var mu sync.Mutex
// getCounter will return the value of a global per-process incrementing counter
func getCounter() int {
return count()
}
// resetCounter will reset incrementing counter's value
func resetCounter() {
counter = 0
}
// count will increment the atomic counter and return its value
func count() int {
mu.Lock()
counter++
mu.Unlock()
return counter
}
// StringInSlice will return whether string exists in given slice or not
func StringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}

+ 95
- 0
vendor/github.com/amalfra/maildir/maildir.go View File

@ -0,0 +1,95 @@
package maildir
import (
"errors"
"os"
"path/filepath"
"sort"
"strings"
"github.com/amalfra/maildir/lib"
)
// Maildir implements maildir format and it's operations
type Maildir struct {
path string
}
// NewMaildir will create new maildir at specified path
func NewMaildir(path string) *Maildir {
maildir := new(Maildir)
maildir.path = path
_, dCur := os.Stat(path + "/cur")
_, dTmp := os.Stat(path + "/tmp")
_, dNew := os.Stat(path + "/new")
if os.IsNotExist(dCur) || os.IsNotExist(dTmp) || os.IsNotExist(dNew) {
maildir.createDirectories()
}
return maildir
}
// createDirectories will the sub directories required by maildir
func (m *Maildir) createDirectories() {
for _, subDir := range lib.Subdirs {
os.MkdirAll(filepath.Join(m.path, subDir), os.ModePerm)
}
}
// Add writes data out as a new message. Returns Message instance
func (m *Maildir) Add(data string) (*lib.Message, error) {
msg, err := lib.NewMessage(m.path)
if err != nil {
return nil, errors.New("failed to create message")
}
err = msg.Write(data)
if err != nil {
return nil, errors.New("failed to write message")
}
return msg, nil
}
// Get returns a message object for key
func (m *Maildir) Get(key string) *lib.Message {
return lib.LoadMessage(m.path, key)
}
// List returns an array of messages from new or cur directory, sorted by key
func (m *Maildir) List(dir string) (map[string]*lib.Message, error) {
if !lib.StringInSlice(dir, lib.Subdirs) {
return nil, errors.New("dir must be :new, :cur, or :tmp")
}
keys, err := m.getDirListing(dir)
if err != nil {
return nil, errors.New("failed to get directory listing")
}
sort.Sort(sort.StringSlice(keys))
// map keys to message objects
keyMap := make(map[string]*lib.Message)
for _, key := range keys {
keyMap[key] = m.Get(key)
}
return keyMap, nil
}
// getDirListing returns an array of keys in dir
func (m *Maildir) getDirListing(dir string) ([]string, error) {
filter := "*"
searchPath := filepath.Join(m.path, dir, filter)
filePaths, err := filepath.Glob(searchPath)
// remove maildir path so that only key remains
for i, filePath := range filePaths {
filePaths[i] = strings.TrimPrefix(filePath, m.path)
}
return filePaths, err
}
// Delete a message by key
func (m *Maildir) Delete(key string) error {
return m.Get(key).Destroy()
}

+ 3
- 0
vendor/modules.txt View File

@ -0,0 +1,3 @@
# github.com/amalfra/maildir v0.0.7
github.com/amalfra/maildir
github.com/amalfra/maildir/lib

+ 60
- 0
zangtumb.go View File

@ -0,0 +1,60 @@
package main
import (
"log"
"os"
"zangtumb/smtpd"
)
var ZangSmtpServer *smtpd.Server
var KeyFile, CrtFile, ServerName, ListenAddr string
const AppName = "ZangTumb"
func init() {
ZangSmtpServer = new(smtpd.Server)
KeyFile = os.Getenv("KEYFILE")
log.Println("KeyFile: ", KeyFile)
CrtFile = os.Getenv("CERTFILE")
log.Println("CrtFile: ", CrtFile)
ServerName = os.Getenv("DOMAINNAME")
log.Println("ServerName: ", ServerName)
ListenAddr = os.Getenv("LISTEN")
log.Println("ListenAddr: ", ListenAddr)
ZangSmtpServer.AuthRequired = false
ZangSmtpServer.Hostname = ServerName
ZangSmtpServer.MaxRcpt = SmtpBackend.MaxRecipients
if ZangSmtpServer.MaxRcpt == 0 {
ZangSmtpServer.MaxRcpt = 1
log.Println("No recipients, setting MaxRcpt to 1")
} else {
log.Println("MaxRcpt: ", ZangSmtpServer.MaxRcpt)
}
if os.Getenv("USETLS") == "true" {
ZangSmtpServer.ConfigureTLS(CrtFile, KeyFile)
ZangSmtpServer.TLSListener = false
ZangSmtpServer.TLSRequired = true
log.Println("Using TLS")
} else {
ZangSmtpServer.TLSListener = false
ZangSmtpServer.TLSRequired = false
log.Println("WARNING: NOT Using TLS")
}
}
func main() {
if err := smtpd.ListenAndServe(ListenAddr, mailHandler, AppName, ServerName, ZangSmtpServer); err != nil {
log.Panicln("Ops. Something went wrong: ", err.Error())
}
}

Loading…
Cancel
Save