func MailView(w http.ResponseWriter, r *http.Request, ctx *Context) (err error) { id := ctx.Vars["id"] log.LogTrace("Loading Mail <%s> from Mongodb", id) //we need a user to sign to if ctx.User == nil { log.LogTrace("This page requires a login.") ctx.Session.AddFlash("This page requires a login.") return LoginForm(w, r, ctx) } m, err := ctx.Ds.Load(id) if err == nil { ctx.Ds.Messages.Update( bson.M{"id": m.Id}, bson.M{"$set": bson.M{"unread": false}}, ) return RenderTemplate("mailbox/_show.html", w, map[string]interface{}{ "ctx": ctx, "title": "Mail", "message": m, }) } else { http.NotFound(w, r) return } }
func MailAttachment(w http.ResponseWriter, r *http.Request, ctx *Context) (err error) { id := ctx.Vars["id"] log.LogTrace("Loading Attachment <%s> from Mongodb", id) //we need a user to sign to if ctx.User == nil { log.LogTrace("This page requires a login.") ctx.Session.AddFlash("This page requires a login.") return LoginForm(w, r, ctx) } m, err := ctx.Ds.LoadAttachment(id) if err != nil { return fmt.Errorf("ID provided is invalid: %v", err) } if len(m.Attachments) > 0 { at := m.Attachments[0] data, err := base64.StdEncoding.DecodeString(at.Body) if err != nil { return fmt.Errorf("Cannot decode attachment: %v", err) } reader := bytes.NewReader(data) w.Header().Set("Content-Type", at.ContentType) //w.Header().Set("Content-Disposition", "attachment; filename=\""+at.FileName+"\"") http.ServeContent(w, r, at.FileName, time.Now(), reader) return nil } else { http.NotFound(w, r) return } }
// TODO support nested MIME content func ParseSMTPMessage(m *config.SMTPMessage, hostname string, mimeParser bool) *Message { arr := make([]*Path, 0) for _, path := range m.To { arr = append(arr, PathFromString(path)) } msg := &Message{ Id: bson.NewObjectId().Hex(), From: PathFromString(m.From), To: arr, Created: time.Now(), Ip: m.Host, Unread: true, Starred: false, } if mimeParser { msg.Content = &Content{Size: len(m.Data), Headers: make(map[string][]string, 0), Body: m.Data} // Read mail using standard mail package if rm, err := mail.ReadMessage(bytes.NewBufferString(m.Data)); err == nil { log.LogTrace("Reading Mail Message") msg.Content.Size = len(m.Data) msg.Content.Headers = rm.Header msg.Subject = MimeHeaderDecode(rm.Header.Get("Subject")) if mt, p, err := mime.ParseMediaType(rm.Header.Get("Content-Type")); err == nil { if strings.HasPrefix(mt, "multipart/") { log.LogTrace("Parsing MIME Message") MIMEBody := &MIMEBody{Parts: make([]*MIMEPart, 0)} if err := ParseMIME(MIMEBody, rm.Body, p["boundary"], msg); err == nil { log.LogTrace("Got multiparts %d", len(MIMEBody.Parts)) msg.MIME = MIMEBody } } else { setMailBody(rm, msg) } } else { setMailBody(rm, msg) } } else { msg.Content.TextBody = m.Data } } else { msg.Content = ContentFromString(m.Data) } recd := fmt.Sprintf("from %s ([%s]) by %s (Smtpd)\r\n for <%s>; %s\r\n", m.Helo, m.Host, hostname, msg.Id+"@"+hostname, time.Now().Format(time.RFC1123Z)) //msg.Content.Headers["Delivered-To"] = []string{msg.To} msg.Content.Headers["Message-ID"] = []string{msg.Id + "@" + hostname} msg.Content.Headers["Received"] = []string{recd} msg.Content.Headers["Return-Path"] = []string{"<" + m.From + ">"} return msg }
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 }
// 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) } }
// 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) }
func Stop() { log.LogTrace("HTTP shutdown requested") shutdown = true if listener != nil { listener.Close() } else { log.LogError("HTTP listener was nil during shutdown") } }
// openLogFile creates or appends to the logfile passed on commandline func openLogFile() error { // use specified log file var err error logf, err = os.OpenFile(*logfile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) if err != nil { return fmt.Errorf("Failed to create %v: %v\n", *logfile, err) } golog.SetOutput(logf) log.LogTrace("Opened new logfile") return nil }
func Register(w http.ResponseWriter, req *http.Request, ctx *Context) error { if ctx.User != nil { ctx.Session.AddFlash("Already logged in") http.Redirect(w, req, reverse("Mails"), http.StatusSeeOther) } r := &data.LoginForm{ Username: req.FormValue("username"), Password: req.FormValue("password"), } if r.Validate() { result := &data.User{} err := ctx.Ds.Users.Find(bson.M{"username": r.Username}).One(&result) if err == nil { ctx.Session.AddFlash("User already exists!") return RegisterForm(w, req, ctx) } u := &data.User{ Id: bson.NewObjectId(), Firstname: req.FormValue("firstname"), Lastname: req.FormValue("lastname"), Email: req.FormValue("email"), Username: r.Username, IsActive: false, JoinedAt: time.Now(), LastLoginIp: ctx.ClientIp, } u.SetPassword(r.Password) if err := ctx.Ds.Users.Insert(u); err != nil { ctx.Session.AddFlash("Problem registering user.") return RegisterForm(w, req, ctx) } if u.IsActive { //store the user id in the values and redirect to index ctx.Session.Values["user"] = u.Id.Hex() ctx.Session.AddFlash("Registration successful") http.Redirect(w, req, reverse("Mails"), http.StatusSeeOther) return nil } else { log.LogTrace("Registration successful") ctx.Session.AddFlash("Registration successful") return LoginForm(w, req, ctx) } } else { ctx.Session.AddFlash("Please fill all fields!") return RegisterForm(w, req, ctx) } return fmt.Errorf("Failed to register!") }
// ParseTemplate loads the requested template along with _base.html, caching // the result (if configured to do so) func ParseTemplate(name string, partial bool) (*template.Template, error) { cachedMutex.Lock() defer cachedMutex.Unlock() if t, ok := cachedTemplates[name]; ok { return t, nil } tempPath := strings.Replace(name, "/", string(filepath.Separator), -1) tempFile := filepath.Join(webConfig.TemplateDir, tempPath) log.LogTrace("Parsing template %v", tempFile) var err error var t *template.Template if partial { // Need to get basename of file to make it root template w/ funcs base := path.Base(name) t = template.New(base).Funcs(TemplateFuncs) t, err = t.ParseFiles(tempFile) } else { t = template.New("layout.html").Funcs(TemplateFuncs) // Note that the layout file must be the first parameter in ParseFiles t, err = t.ParseFiles(filepath.Join(webConfig.TemplateDir, "layout.html"), tempFile) } if err != nil { return nil, err } // Allows us to disable caching for theme development if webConfig.TemplateCache { if partial { log.LogTrace("Caching partial %v", name) cachedTemplates[name] = t } else { log.LogTrace("Caching template %v", name) cachedTemplates[name] = t } } return t, nil }
// If running Nginx as a proxy, give Nginx the IP address and port for the SMTP server // Primary use of Nginx is to terminate TLS so that Go doesn't need to deal with it. // This could perform auth and load balancing too // See http://wiki.nginx.org/MailCoreModule func NginxHTTPAuth(w http.ResponseWriter, r *http.Request, ctx *Context) error { //log.LogTrace("Nginx Auth Client: %s", parseRemoteAddr(r)) log.LogTrace("Nginx Auth Client IP <%s> (%s)", r.Header.Get("Client-IP"), r.Header.Get("Client-Host")) cfg := config.GetSmtpConfig() w.Header().Add("Auth-Status", "OK") w.Header().Add("Auth-Server", cfg.Ip4address.String()) w.Header().Add("Auth-Port", strconv.Itoa(cfg.Ip4port)) fmt.Fprint(w, "") return nil }
func MailList(w http.ResponseWriter, r *http.Request, ctx *Context) (err error) { log.LogTrace("Loading Mails from Mongodb") page, _ := strconv.Atoi(ctx.Vars["page"]) limit := 25 //we need a user to sign to if ctx.User == nil { log.LogTrace("This page requires a login.") ctx.Session.AddFlash("This page requires a login.") return LoginForm(w, r, ctx) } t, err := ctx.Ds.Total() if err != nil { http.NotFound(w, r) return } p := NewPagination(t, limit, page, "/mails") if page > p.Pages() { http.NotFound(w, r) return } messages, err := ctx.Ds.List(p.Offset(), p.Limit()) if err == nil { return RenderTemplate("mailbox/_list.html", w, map[string]interface{}{ "ctx": ctx, "title": "Mails", "messages": messages, "end": p.Offset() + p.Limit(), "pagination": p, }) } else { http.NotFound(w, r) return } }
func Login(w http.ResponseWriter, req *http.Request, ctx *Context) error { l := &data.LoginForm{ Username: req.FormValue("username"), Password: req.FormValue("password"), } if l.Validate() { u, err := ctx.Ds.Login(l.Username, l.Password) if err == nil { //store the user id in the values and redirect to index log.LogTrace("Login successful for session <%v>", u.Id) ctx.Ds.Users.Update( bson.M{"_id": u.Id}, bson.M{"$set": bson.M{"lastlogintime": time.Now(), "lastloginip": ctx.ClientIp}, "$inc": bson.M{"logincount": 1}}, ) if u.IsActive { ctx.Session.Values["user"] = u.Id.Hex() http.Redirect(w, req, reverse("Mails"), http.StatusSeeOther) return nil } else { log.LogTrace("The user is not activated") ctx.Session.AddFlash("Username is not activated") return LoginForm(w, req, ctx) } } else { log.LogTrace("Invalid Username/Password") ctx.Session.AddFlash("Invalid Username/Password") return LoginForm(w, req, ctx) } } else { ctx.Session.AddFlash("Please fill all fields!") return LoginForm(w, req, ctx) } return fmt.Errorf("Failed to login!") }
func MailDelete(w http.ResponseWriter, r *http.Request, ctx *Context) (err error) { id := ctx.Vars["id"] log.LogTrace("Delete Mail <%s> from Mongodb", id) //we need a user to sign to if ctx.User == nil { log.LogTrace("This page requires a login.") ctx.Session.AddFlash("This page requires a login.") return LoginForm(w, r, ctx) } err = ctx.Ds.DeleteOne(id) if err == nil { log.LogTrace("Deleted mail id: %s", id) ctx.Session.AddFlash("Successfuly deleted mail id:" + id) http.Redirect(w, r, reverse("Mails"), http.StatusSeeOther) return nil } else { http.NotFound(w, r) return err } }
func Status(w http.ResponseWriter, r *http.Request, ctx *Context) (err error) { //we need a user to sign to if ctx.User == nil { log.LogTrace("This page requires a login.") ctx.Session.AddFlash("This page requires a login.") return LoginForm(w, r, ctx) } updateSystemStatus() return RenderTemplate("root/status.html", w, map[string]interface{}{ "ctx": ctx, "SysStatus": sysStatus, }) }
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) } } } }
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 (c *Client) tlsHandler() { if c.tls_on { c.Write("502", "Already running in TLS") return } if c.server.TLSConfig == nil { c.Write("502", "TLS not supported") return } log.LogTrace("Ready to start TLS") c.Write("220", "Ready to start TLS") // upgrade to TLS var tlsConn *tls.Conn tlsConn = tls.Server(c.conn, c.server.TLSConfig) err := tlsConn.Handshake() // not necessary to call here, but might as well if err == nil { //c.conn = net.Conn(tlsConn) c.conn = tlsConn c.bufin = bufio.NewReader(c.conn) c.bufout = bufio.NewWriter(c.conn) c.tls_on = true // Reset envelope as a new EHLO/HELO is required after STARTTLS c.reset() // Reset deadlines on the underlying connection before I replace it // with a TLS connection c.conn.SetDeadline(time.Time{}) c.flush() } else { c.logWarn("Could not TLS handshake:%v", err) c.Write("550", "Handshake error") } c.state = 1 }
func ContentFromString(data string) *Content { log.LogTrace("Parsing Content from string: <%d>", len(data)) x := strings.SplitN(data, "\r\n\r\n", 2) h := make(map[string][]string, 0) if len(x) == 2 { headers, body := x[0], x[1] hdrs := strings.Split(headers, "\r\n") var lastHdr = "" for _, hdr := range hdrs { if lastHdr != "" && strings.HasPrefix(hdr, " ") { h[lastHdr][len(h[lastHdr])-1] = h[lastHdr][len(h[lastHdr])-1] + hdr } else if strings.Contains(hdr, ": ") { y := strings.SplitN(hdr, ": ", 2) key, value := y[0], y[1] // TODO multiple header fields h[key] = []string{value} lastHdr = key } else { log.LogWarn("Found invalid header: '%s'", hdr) } } //log.LogTrace("Found body: '%s'", body) return &Content{ Size: len(data), Headers: h, Body: body, //Body: "", } } else { return &Content{ Size: len(data), Headers: h, Body: x[0], TextBody: x[0], } } }
func Ping(w http.ResponseWriter, r *http.Request, ctx *Context) error { log.LogTrace("Ping successful") fmt.Fprint(w, "OK") return nil }
// 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, }) } } }
// Stop requests the SMTP server closes it's listener func (s *Server) Stop() { log.LogTrace("SMTP shutdown requested, connections will be drained") s.shutdown = true s.listener.Close() }
// closeLogFile closes the current logfile func closeLogFile() error { log.LogTrace("Closing logfile") return logf.Close() }
// Drain causes the caller to block until all active SMTP sessions have finished func (s *Server) Drain() { s.waitgroup.Wait() log.LogTrace("SMTP connections drained") }
// Session specific logging methods func (c *Client) logTrace(msg string, args ...interface{}) { log.LogTrace("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 }