// ServeHTTP builds the context and passes onto the real handler func (h handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { // Create the context ctx, err := NewContext(req) if err != nil { log.LogError("Failed to create context: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } defer ctx.Close() // Run the handler, grab the error, and report it buf := new(httpbuf.Buffer) log.LogTrace("Web: %v %v %v %v", parseRemoteAddr(req), req.Proto, req.Method, req.RequestURI) err = h(buf, req, ctx) if err != nil { log.LogError("Error handling %v: %v", req.RequestURI, err) http.Error(w, err.Error(), http.StatusInternalServerError) return } // Save the session if err = ctx.Session.Save(req, buf); err != nil { log.LogError("Failed to save session: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } // Apply the buffered response to the writer buf.Apply(w) }
// Start() the web server func Start() { addr := fmt.Sprintf("%v:%v", webConfig.Ip4address, webConfig.Ip4port) server := &http.Server{ Addr: addr, Handler: nil, ReadTimeout: 60 * time.Second, WriteTimeout: 60 * time.Second, } // We don't use ListenAndServe because it lacks a way to close the listener log.LogInfo("HTTP listening on TCP4 %v", addr) var err error listener, err = net.Listen("tcp", addr) if err != nil { log.LogError("HTTP failed to start TCP4 listener: %v", err) // TODO More graceful early-shutdown procedure panic(err) } err = server.Serve(listener) if shutdown { log.LogTrace("HTTP server shutting down on request") } else if err != nil { log.LogError("HTTP server failed: %v", err) } }
//Login validates and returns a user object if they exist in the database. func (mongo *MongoDB) Login(username, password string) (*User, error) { u := &User{} err := mongo.Users.Find(bson.M{"username": username}).One(&u) if err != nil { log.LogError("Login error: %v", err) return nil, err } if ok := Validate_Password(u.Password, password); !ok { log.LogError("Invalid Password: %s", u.Username) return nil, fmt.Errorf("Invalid Password!") } return u, nil }
// Debug mail data to file func (c *Client) saveMailDatatoFile(msg string) { filename := fmt.Sprintf("%s/%s-%s-%s.raw", c.server.DebugPath, c.remoteHost, c.from, time.Now().Format("Jan-2-2006-3:04:00pm")) f, err := os.Create(filename) if err != nil { log.LogError("Error saving file %v", err) } defer f.Close() n, err := io.WriteString(f, msg) if err != nil { log.LogError("Error saving file %v: %v", n, err) } }
// signalProcessor is a goroutine that handles OS signals func signalProcessor(c <-chan os.Signal) { for { sig := <-c switch sig { case syscall.SIGHUP: // Rotate logs if configured if logf != nil { log.LogInfo("Recieved SIGHUP, cycling logfile") closeLogFile() openLogFile() } else { log.LogInfo("Ignoring SIGHUP, logfile not configured") } case syscall.SIGTERM: // Initiate shutdown log.LogInfo("Received SIGTERM, shutting down") go timedExit() web.Stop() if smtpServer != nil { smtpServer.Stop() } else { log.LogError("smtpServer was nil during shutdown") } } } }
func (mongo *MongoDB) IsGreyMail(email, t string) (int, error) { tl, err := mongo.Emails.Find(bson.M{"email": email, "type": t, "isactive": true}).Count() if err != nil { log.LogError("Error checking email greylist: %s", err) return -1, err } return tl, nil }
func (mongo *MongoDB) StoreGreyHost(h *GreyHost) (string, error) { err := mongo.Hosts.Insert(h) if err != nil { log.LogError("Error inserting greylist ip: %s", err) return "", err } return h.Id.Hex(), nil }
func (mongo *MongoDB) IsGreyHost(hostname string) (int, error) { tl, err := mongo.Hosts.Find(bson.M{"hostname": hostname, "isactive": true}).Count() if err != nil { log.LogError("Error checking host greylist: %s", err) return -1, err } return tl, nil }
func (mongo *MongoDB) Total() (int, error) { total, err := mongo.Messages.Find(bson.M{}).Count() if err != nil { log.LogError("Error loading message: %s", err) return -1, err } return total, nil }
func (mongo *MongoDB) StoreGreyMail(m *GreyMail) (string, error) { err := mongo.Emails.Insert(m) if err != nil { log.LogError("Error inserting greylist email: %s", err) return "", err } return m.Id.Hex(), nil }
func (mongo *MongoDB) StoreSpamIp(s SpamIP) (string, error) { err := mongo.Spamdb.Insert(s) if err != nil { log.LogError("Error inserting greylist ip: %s", err) return "", err } return s.Id.Hex(), nil }
func (mongo *MongoDB) Store(m *Message) (string, error) { err := mongo.Messages.Insert(m) if err != nil { log.LogError("Error inserting message: %s", err) return "", err } return m.Id, nil }
func Stop() { log.LogTrace("HTTP shutdown requested") shutdown = true if listener != nil { listener.Close() } else { log.LogError("HTTP listener was nil during shutdown") } }
func (mongo *MongoDB) Load(id string) (*Message, error) { result := &Message{} err := mongo.Messages.Find(bson.M{"id": id}).One(&result) if err != nil { log.LogError("Error loading message: %s", err) return nil, err } return result, nil }
// RenderPartial fetches the named template and renders it to the provided // ResponseWriter. func RenderPartial(name string, w http.ResponseWriter, data interface{}) error { t, err := ParseTemplate(name, true) if err != nil { log.LogError("Error in template '%v': %v", name, err) return err } w.Header().Set("Expires", "-1") return t.Execute(w, data) }
func getSession(c config.DataStoreConfig) *mgo.Session { if mgoSession == nil { var err error mgoSession, err = mgo.Dial(c.MongoUri) if err != nil { log.LogError("Session Error connecting to MongoDB: %s", err) return nil } } return mgoSession.Clone() }
func (mongo *MongoDB) LoadAttachment(id string) (*Message, error) { result := &Message{} err := mongo.Messages.Find(bson.M{"attachments.id": id}).Select(bson.M{ "id": 1, "attachments.$": 1, }).One(&result) if err != nil { log.LogError("Error loading attachment: %s", err) return nil, err } return result, nil }
func (ds *DataStore) SaveSpamIP(ip string, email string) { s := SpamIP{ Id: bson.NewObjectId(), CreatedAt: time.Now(), IsActive: true, Email: email, IPAddress: ip, } if _, err := ds.Storage.(*MongoDB).StoreSpamIp(s); err != nil { log.LogError("Error inserting Spam IPAddress: %s", err) } }
func GreyMailFromAdd(w http.ResponseWriter, r *http.Request, ctx *Context) (err error) { id := ctx.Vars["id"] log.LogTrace("Greylist add mail %s", id) //we need a user to sign to if ctx.User == nil { log.LogWarn("Please login to add to grey list!") http.NotFound(w, r) return } // we need a user to be admin if ctx.User.IsSuperuser == false { http.NotFound(w, r) return } // we need to load email m, err := ctx.Ds.Load(id) if err != nil { log.LogTrace("Greylist mail Id not found %s", id) http.NotFound(w, r) return } e := fmt.Sprintf("%s@%s", m.From.Mailbox, m.From.Domain) if to, _ := ctx.Ds.IsGreyMail(e, "from"); to == 0 { log.LogTrace("Greylist inserting mail %s", e) gm := data.GreyMail{ Id: bson.NewObjectId(), CreatedBy: ctx.User.Id.Hex(), CreatedAt: time.Now(), IsActive: true, Email: e, Local: m.From.Mailbox, Domain: m.From.Domain, Type: "from", } if err = ctx.Ds.Emails.Insert(gm); err != nil { log.LogError("Error inserting grey list: %s", err) http.NotFound(w, r) return } return } http.NotFound(w, r) return }
// Reversable routing function (shared with templates) func reverse(name string, things ...interface{}) string { // Convert the things to strings strs := make([]string, len(things)) for i, th := range things { strs[i] = fmt.Sprint(th) } // Grab the route u, err := Router.Get(name).URL(strs...) if err != nil { log.LogError("Failed to reverse route: %v", err) return "/ROUTE-ERROR" } return u.Path }
func (mongo *MongoDB) List(start int, limit int) (*Messages, error) { messages := &Messages{} err := mongo.Messages.Find(bson.M{}).Sort("-_id").Skip(start).Limit(limit).Select(bson.M{ "id": 1, "from": 1, "to": 1, "attachments": 1, "created": 1, "ip": 1, "subject": 1, "starred": 1, "unread": 1, }).All(messages) if err != nil { log.LogError("Error loading messages: %s", err) return nil, err } return messages, nil }
func CreateMongoDB(c config.DataStoreConfig) *MongoDB { log.LogTrace("Connecting to MongoDB: %s\n", c.MongoUri) session, err := mgo.Dial(c.MongoUri) if err != nil { log.LogError("Error connecting to MongoDB: %s", err) return nil } return &MongoDB{ Config: c, Session: session, Messages: session.DB(c.MongoDb).C(c.MongoColl), Users: session.DB(c.MongoDb).C("Users"), Hosts: session.DB(c.MongoDb).C("GreyHosts"), Emails: session.DB(c.MongoDb).C("GreyMails"), Spamdb: session.DB(c.MongoDb).C("SpamDB"), } }
func (ds *DataStore) SaveMail() { log.LogTrace("Running SaveMail Rotuines") var err error var recon bool for { mc := <-ds.SaveMailChan msg := ParseSMTPMessage(mc, mc.Domain, ds.Config.MimeParser) if ds.Config.Storage == "mongodb" { mc.Hash, err = ds.Storage.(*MongoDB).Store(msg) // if mongo conection is broken, try to reconnect only once if err == io.EOF && !recon { log.LogWarn("Connection error trying to reconnect") ds.Storage = CreateMongoDB(ds.Config) recon = true //try to save again mc.Hash, err = ds.Storage.(*MongoDB).Store(msg) } if err == nil { recon = false log.LogTrace("Save Mail Client hash : <%s>", mc.Hash) mc.Notify <- 1 //Notify web socket ds.NotifyMailChan <- mc.Hash } else { mc.Notify <- -1 log.LogError("Error storing message: %s", err) } } } }
// Main listener loop func (s *Server) Start() { cfg := config.GetSmtpConfig() log.LogTrace("Loading the certificate: %s", cfg.PubKey) cert, err := tls.LoadX509KeyPair(cfg.PubKey, cfg.PrvKey) if err != nil { log.LogError("There was a problem with loading the certificate: %s", err) } else { s.TLSConfig = &tls.Config{ Certificates: []tls.Certificate{cert}, ClientAuth: tls.VerifyClientCertIfGiven, ServerName: cfg.Domain, } //s.TLSConfig .Rand = rand.Reader } defer s.Stop() addr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%v:%v", cfg.Ip4address, cfg.Ip4port)) if err != nil { log.LogError("Failed to build tcp4 address: %v", err) // TODO More graceful early-shutdown procedure //panic(err) s.Stop() return } // Start listening for SMTP connections log.LogInfo("SMTP listening on TCP4 %v", addr) s.listener, err = net.ListenTCP("tcp4", addr) if err != nil { log.LogError("SMTP failed to start tcp4 listener: %v", err) // TODO More graceful early-shutdown procedure //panic(err) s.Stop() return } //Connect database s.Store.StorageConnect() var tempDelay time.Duration var clientId int64 // Handle incoming connections for clientId = 1; ; clientId++ { if conn, err := s.listener.Accept(); err != nil { if nerr, ok := err.(net.Error); ok && nerr.Temporary() { // Temporary error, sleep for a bit and try again if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } log.LogError("SMTP accept error: %v; retrying in %v", err, tempDelay) time.Sleep(tempDelay) continue } else { if s.shutdown { log.LogTrace("SMTP listener shutting down on request") return } // TODO Implement a max error counter before shutdown? // or maybe attempt to restart smtpd panic(err) } } else { tempDelay = 0 s.waitgroup.Add(1) log.LogInfo("There are now %s serving goroutines", strconv.Itoa(runtime.NumGoroutine())) host, _, _ := net.SplitHostPort(conn.RemoteAddr().String()) s.sem <- 1 // Wait for active queue to drain. go s.handleClient(&Client{ state: 1, server: s, conn: conn, remoteHost: host, time: time.Now().Unix(), bufin: bufio.NewReader(conn), bufout: bufio.NewWriter(conn), id: clientId, }) } } }
func (c *Client) logError(msg string, args ...interface{}) { // Update metrics //expErrorsTotal.Add(1) log.LogError("SMTP[%v]<%v> %v", c.remoteHost, c.id, fmt.Sprintf(msg, args...)) }
func ParseMIME(MIMEBody *MIMEBody, reader io.Reader, boundary string, message *Message) error { mr := multipart.NewReader(reader, boundary) for { mrp, err := mr.NextPart() if err != nil { if err == io.EOF { // This is a clean end-of-message signal break //log.Fatal("Error eof %s", err) } return err } if len(mrp.Header) == 0 { // Empty header probably means the part didn't using the correct trailing "--" // syntax to close its boundary. We will let this slide if this this the // last MIME part. if _, err := mr.NextPart(); err != nil { if err == io.EOF || strings.HasSuffix(err.Error(), "EOF") { // This is what we were hoping for break } else { return fmt.Errorf("Error at boundary %v: %v", boundary, err) } } return fmt.Errorf("Empty header at boundary %v", boundary) } ctype := mrp.Header.Get("Content-Type") if ctype == "" { fmt.Errorf("Missing Content-Type at boundary %v", boundary) } mediatype, mparams, err := mime.ParseMediaType(ctype) if err != nil { return err } encoding := mrp.Header.Get("Content-Transfer-Encoding") // Figure out our disposition, filename disposition, dparams, err := mime.ParseMediaType(mrp.Header.Get("Content-Disposition")) if strings.HasPrefix(mediatype, "multipart/") && mparams["boundary"] != "" { // Content is another multipart ParseMIME(MIMEBody, mrp, mparams["boundary"], message) } else { if n, body, err := Partbuf(mrp); err == nil { part := &MIMEPart{Size: int(n), Headers: mrp.Header, Body: string(body), FileName: ""} // Disposition is optional part.Disposition = disposition part.ContentType = mediatype part.TransferEncoding = encoding if mparams["charset"] != "" { part.Charset = mparams["charset"] } if disposition == "attachment" || disposition == "inline" { //log.LogTrace("Found attachment: '%s'", disposition) part.FileName = MimeHeaderDecode(dparams["filename"]) if part.FileName == "" && mparams["name"] != "" { part.FileName = MimeHeaderDecode(mparams["name"]) } } // Save attachments if disposition == "attachment" && len(part.FileName) > 0 { log.LogTrace("Found attachment: '%s'", disposition) //db.messages.find({ 'attachments.id': "54200a938b1864264c000005" }, {"attachments.$" : 1}) attachment := &Attachment{ Id: bson.NewObjectId().Hex(), Body: string(body), FileName: part.FileName, Charset: part.Charset, ContentType: mediatype, TransferEncoding: encoding, Size: int(n), } message.Attachments = append(message.Attachments, attachment) } else { MIMEBody.Parts = append(MIMEBody.Parts, part) } //use mediatype; ctype will have 'text/plain; charset=UTF-8' // attachments might be plain text content, so make sure of it if mediatype == "text/plain" && disposition != "attachment" { message.Content.TextBody = MimeBodyDecode(string(body), part.Charset, part.TransferEncoding) } if mediatype == "text/html" && disposition != "attachment" { message.Content.HtmlBody = MimeBodyDecode(string(body), part.Charset, part.TransferEncoding) } } else { log.LogError("Error Processing MIME message: <%s>", err) } } } return nil }
// timedExit is called as a goroutine during shutdown, it will force an exit after 15 seconds func timedExit() { time.Sleep(15 * time.Second) log.LogError("Smtpd clean shutdown timed out, forcing exit") os.Exit(0) }
func main() { flag.Parse() runtime.GOMAXPROCS(runtime.NumCPU()) if *help { flag.Usage() return } // Load & Parse config /* if flag.NArg() != 1 { flag.Usage() os.Exit(1) }*/ //err := config.LoadConfig(flag.Arg(0)) err := config.LoadConfig(*configfile) if err != nil { fmt.Fprintf(os.Stderr, "Failed to parse config: %v\n", err) os.Exit(1) } // Setup signal handler sigChan := make(chan os.Signal) signal.Notify(sigChan, syscall.SIGHUP, syscall.SIGTERM) go signalProcessor(sigChan) // Configure logging, close std* fds level, _ := config.Config.String("logging", "level") log.SetLogLevel(level) if *logfile != "stderr" { // stderr is the go logging default if *logfile == "stdout" { // set to stdout golog.SetOutput(os.Stdout) } else { err := openLogFile() if err != nil { fmt.Fprintf(os.Stderr, "%v", err) os.Exit(1) } defer closeLogFile() // close std* streams os.Stdout.Close() os.Stderr.Close() // Warning: this will hide panic() output os.Stdin.Close() os.Stdout = logf os.Stderr = logf } } log.LogInfo("Smtpd %v (%v) starting...", VERSION, BUILD_DATE) // Write pidfile if requested // TODO: Probably supposed to remove pidfile during shutdown if *pidfile != "none" { pidf, err := os.Create(*pidfile) if err != nil { log.LogError("Failed to create %v: %v", *pidfile, err) os.Exit(1) } defer pidf.Close() fmt.Fprintf(pidf, "%v\n", os.Getpid()) } // Grab our datastore ds := data.NewDataStore() // Start HTTP server //web.Initialize(config.GetWebConfig(), ds) //go web.Start() // Startup SMTP server, block until it exits smtpServer = smtpd.NewSmtpServer(config.GetSmtpConfig(), ds) smtpServer.Start() // Wait for active connections to finish smtpServer.Drain() }