func main() { flag.Parse() var err error dkimConf, err = dkim.NewConf(*dkimD, *dkimS) if err != nil { log.Fatalf("DKIM configuration error: %v", err) } privKey, err = ioutil.ReadFile(*privKeyFile) if err != nil { log.Fatalf("Couldn't read private key: %v", err) } _, err = dkim.New(dkimConf, privKey) if err != nil { log.Fatalf("DKIM error: %v", err) } server := &smtpd.Server{ WelcomeMessage: *welcomeMsg, Handler: handler, } server.ListenAndServe(*inAddr) }
func dkimSign(config *Config, message string) (string, error) { conf, err := dkim.NewConf(config.DKIMDomain, config.DKIMSelector) if err != nil { return "", err } dkim, err := dkim.New(conf, config.DKIMPrivatePEM) if err != nil { return "", err } signed, err := dkim.Sign([]byte(message)) if err != nil { return "", err } return string(signed), nil }
func main() { flag.Parse() keyFile, err := ioutil.ReadFile(*privateKey) if err != nil { log.Fatal(err) } key, err := ioutil.ReadFile(*dkimKey) if err != nil { log.Fatal(err) } dc, err := dkim.NewConf("lavaboom.com", "mailer") if err != nil { log.Fatal(err) } dk, err := dkim.New(dc, key) if err != nil { log.Fatal(err) } session, err := r.Connect(r.ConnectOpts{ Address: *rethinkAddress, Database: *rethinkDatabase, }) if err != nil { log.Fatal(err) } keyring := openpgp.EntityList{} // This is just retarded parts := strings.Split(string(keyFile), "-----\n-----") for n, part := range parts { if n != 0 { part = "-----" + part } if n != len(parts)-1 { part += "-----" } k1, err := openpgp.ReadArmoredKeyRing(strings.NewReader(part)) if err != nil { log.Fatal(err) } keyring = append(keyring, k1...) } http.HandleFunc("/incoming", func(w http.ResponseWriter, req *http.Request) { body, err := ioutil.ReadAll(req.Body) if err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } var event struct { Email string `json:"email"` Account string `json:"account"` } if err := json.Unmarshal(body, &event); err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } cursor, err := r.Table("emails").Get(event.Email).Run(session) if err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } defer cursor.Close() var email *models.Email if err := cursor.One(&email); err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } input := strings.NewReader(email.Body) result, err := armor.Decode(input) if err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } md, err := openpgp.ReadMessage(result.Body, keyring, nil, nil) if err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } contents, err := ioutil.ReadAll(md.UnverifiedBody) if err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } input = strings.NewReader(email.Manifest) result, err = armor.Decode(input) if err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } md, err = openpgp.ReadMessage(result.Body, keyring, nil, nil) if err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } rawman, err := ioutil.ReadAll(md.UnverifiedBody) if err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } manifest, err := man.Parse(rawman) if err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } to := []string{} for _, x := range manifest.To { to = append(to, x.String()) } var contentType string for _, part := range manifest.Parts { if part.ID == "body" { contentType = part.ContentType } } if contentType == "" { contentType = manifest.ContentType } m1 := strings.Replace(`From: `+manifest.From.String()+` To: `+*grooveAddress+` MIME-Version: 1.0 Message-ID: <`+uniuri.NewLen(32)+`@lavaboom.com> Content-Type: `+contentType+` Content-Transfer-Encoding: quoted-printable Subject: `+quotedprintable.EncodeToString([]byte(manifest.Subject))+` `+quotedprintable.EncodeToString(contents), "\n", "\r\n", -1) signed, err := dk.Sign([]byte(m1)) if err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } if err := smtp.SendMail(*forwardingServer, nil, manifest.From.Address, []string{*grooveAddress}, signed); err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } log.Printf("Forwarded email from %s with title %s", manifest.From.String(), manifest.Subject) }) http.ListenAndServe(":8000", nil) }
func process(item interface{}, queue string) { email := item.(email.Email) fmt.Printf("got %s on queue %s\n", email, queue) m := gomail.NewMessage() m.SetHeader("From", email.From) //m.SetHeader("To", "*****@*****.**", "*****@*****.**") // m.SetHeader("To", "Luca Cervasio <*****@*****.**>") m.SetHeader("To", email.To) //m.SetAddressHeader("Cc", "*****@*****.**", "Dan") m.SetHeader("Subject", email.Subject) m.SetHeader("Message-ID", messageId()) // m.SetHeader("Precedence", "bulk") // m.SetHeader("List-Unsubscribe", "http://asdasd") m.SetBody("text/plain", email.Body) // m.SetBody("text/html", email.Body) // m.AddAlternative("text/plain", email.Body) //m.Attach("/home/Alex/lolcat.jpg") // converto l'email in []bytes var b bytes.Buffer m.WriteTo(&b) byt := b.Bytes() // firmo il messaggio con DKIM var privateKey []byte var domain string if email.From == "*****@*****.**" { privateKey, _ = ioutil.ReadFile("dkim-private_spensale.pem") domain = "spensale.it" } else { privateKey, _ = ioutil.ReadFile("dkim-private_moonar.pem") domain = "moonar.it" } conf, _ := dkim.NewConf(domain, "mail") d, _ := dkim.New(conf, privateKey) signed, err := d.Sign(byt) if err != nil { panic(err) } // mando il messaggio firmato a postfix cl, err2 := smtp.Dial("localhost:25") if err2 != nil { log.Fatal(err) } defer cl.Close() // Set the sender and recipient. cl.Mail(email.From) cl.Rcpt(email.To) // cl.Rcpt("*****@*****.**") // Send the email body. wc, err := cl.Data() if err != nil { log.Fatal(err) } defer wc.Close() buf := bytes.NewBufferString(string(signed)) if _, err = buf.WriteTo(wc); err != nil { log.Fatal(err) } }
func StartQueue(config *shared.Flags) { // Initialize a new logger log := logrus.New() if config.LogFormatterType == "text" { log.Formatter = &logrus.TextFormatter{ ForceColors: config.ForceColors, } } else if config.LogFormatterType == "json" { log.Formatter = &logrus.JSONFormatter{} } log.Level = logrus.DebugLevel // Create a new header encoder he := quotedprintable.Q.NewHeaderEncoder("utf-8") // Initialize the database connection session, err := gorethink.Connect(gorethink.ConnectOpts{ Address: config.RethinkAddress, AuthKey: config.RethinkKey, MaxIdle: 10, Timeout: time.Second * 10, }) if err != nil { log.WithFields(logrus.Fields{ "error": err.Error(), }).Fatal("Unable to connect to RethinkDB") } // Create a new producer consumer, err := nsq.NewConsumer("send_email", "receive", nsq.NewConfig()) if err != nil { log.WithFields(logrus.Fields{ "error": err.Error(), }).Fatal("Unable to create a consumer") } // Connect to NSQ producer, err := nsq.NewProducer(config.NSQDAddress, nsq.NewConfig()) if err != nil { log.WithFields(logrus.Fields{ "error": err.Error(), }).Fatal("Unable to connect to NSQd") } // Load a DKIM signer var dkimSigner map[string]*dkim.DKIM if config.DKIMKey != "" { dkimSigner = map[string]*dkim.DKIM{} key, err := ioutil.ReadFile(config.DKIMKey) if err != nil { log.WithFields(logrus.Fields{ "error": err.Error(), }).Fatal("Unable to read DKIM private key") } for domain, _ := range domains { dkimConf, err := dkim.NewConf(domain, config.DKIMSelector) if err != nil { log.WithFields(logrus.Fields{ "error": err.Error(), }).Fatal("Unable to create a new DKIM conf object") } dk, err := dkim.New(dkimConf, key) if err != nil { log.WithFields(logrus.Fields{ "error": err.Error(), }).Fatal("Unable to create a new DKIM signer") } dkimSigner[domain] = dk } } consumer.AddConcurrentHandlers(nsq.HandlerFunc(func(msg *nsq.Message) error { var id string if err := json.Unmarshal(msg.Body, &id); err != nil { return err } // Get the email from the database cursor, err := gorethink.Db(config.RethinkDatabase).Table("emails").Get(id).Run(session) if err != nil { return err } defer cursor.Close() var email *models.Email if err := cursor.One(&email); err != nil { return err } // Get the thread cursor, err = gorethink.Db(config.RethinkDatabase).Table("threads").Get(email.Thread).Run(session) if err != nil { return err } defer cursor.Close() var thread *models.Thread if err := cursor.One(&thread); err != nil { return err } // Get the proper In-Reply-To hasInReplyTo := false inReplyTo := "" // Fetch received emails in the thread cursor, err = gorethink.Db(config.RethinkDatabase).Table("emails").GetAllByIndex("threadStatus", []interface{}{ thread.ID, "received", }).Pluck("date_created", "message_id", "from").OrderBy(gorethink.Desc(gorethink.Row.Field("date_created"))). Filter(func(row gorethink.Term) gorethink.Term { return gorethink.Expr(email.To).Contains(row.Field("from")) }).Limit(1).Run(session) if err != nil { return err } defer cursor.Close() var emid []*models.Email if err := cursor.All(&emid); err != nil { return err } if len(emid) == 1 { hasInReplyTo = true inReplyTo = emid[0].MessageID } // Fetch the files var files []*models.File if email.Files != nil && len(email.Files) > 0 { filesList := []interface{}{} for _, v := range email.Files { filesList = append(filesList, v) } cursor, err = gorethink.Db(config.RethinkDatabase).Table("files").GetAll(filesList...).Run(session) if err != nil { return err } defer cursor.Close() if err := cursor.All(&files); err != nil { return err } } else { files = []*models.File{} } // Fetch the owner cursor, err = gorethink.Db(config.RethinkDatabase).Table("accounts").Get(email.Owner).Run(session) if err != nil { return err } defer cursor.Close() var account *models.Account if err := cursor.One(&account); err != nil { return err } // Declare a contents variable contents := "" ctxFrom := email.From // Check if charset is set if !strings.Contains(email.ContentType, "; charset=") { email.ContentType += "; charset=utf-8" } if email.Kind == "raw" { // Encode the email if files == nil || len(files) == 0 { buffer := &bytes.Buffer{} context := &rawSingleContext{ From: ctxFrom, CombinedTo: strings.Join(email.To, ", "), MessageID: email.MessageID, HasInReplyTo: hasInReplyTo, InReplyTo: inReplyTo, Subject: he.Encode(email.Name), ContentType: email.ContentType, Body: quotedprintable.EncodeToString([]byte(email.Body)), Date: email.DateCreated.Format(time.RubyDate), } if email.CC != nil && len(email.CC) > 0 { context.HasCC = true context.CombinedCC = strings.Join(email.CC, ", ") } if email.ReplyTo != "" { context.HasReplyTo = true context.ReplyTo = email.ReplyTo } if err := rawSingleTemplate.Execute(buffer, context); err != nil { return err } contents = buffer.String() } else { buffer := &bytes.Buffer{} emailFiles := []*emailFile{} for _, file := range files { emailFiles = append(emailFiles, &emailFile{ Encoding: file.Encoding, Name: file.Name, Body: base64.StdEncoding.EncodeToString([]byte(file.Data)), }) } context := &rawMultiContext{ From: ctxFrom, CombinedTo: strings.Join(email.To, ", "), MessageID: email.MessageID, HasInReplyTo: hasInReplyTo, InReplyTo: inReplyTo, Boundary1: uniuri.NewLen(20), Subject: he.Encode(email.Name), ContentType: email.ContentType, Body: quotedprintable.EncodeToString([]byte(email.Body)), Files: emailFiles, Date: email.DateCreated.Format(time.RubyDate), } if email.CC != nil && len(email.CC) > 0 { context.HasCC = true context.CombinedCC = strings.Join(email.CC, ", ") } if email.ReplyTo != "" { context.HasReplyTo = true context.ReplyTo = email.ReplyTo } if err := rawMultiTemplate.Execute(buffer, context); err != nil { return err } contents = buffer.String() } // Fetch owner's account cursor, err = gorethink.Db(config.RethinkDatabase).Table("accounts").Get(email.Owner).Run(session) if err != nil { return err } defer cursor.Close() var account *models.Account if err := cursor.One(&account); err != nil { return err } // Get owner's key var key *models.Key if account.PublicKey != "" { cursor, err = gorethink.Db(config.RethinkDatabase).Table("keys").Get(account.PublicKey).Run(session) if err != nil { return err } defer cursor.Close() if err := cursor.One(&key); err != nil { return err } } else { cursor, err = gorethink.Db(config.RethinkDatabase).Table("keys").GetAllByIndex("owner", account.ID).Run(session) if err != nil { return err } defer cursor.Close() var keys []*models.Key if err := cursor.All(&keys); err != nil { return err } key = keys[0] } // Parse the key keyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(key.Key)) if err != nil { return err } // From, to and cc parsing fromAddr, err := mail.ParseAddress(email.From) if err != nil { fromAddr = &mail.Address{ Address: email.From, } } toAddr, err := mail.ParseAddressList(strings.Join(email.To, ", ")) if err != nil { toAddr = []*mail.Address{} for _, addr := range email.To { toAddr = append(toAddr, &mail.Address{ Address: addr, }) } } // Prepare a new manifest manifest := &man.Manifest{ Version: semver.Version{ Major: 1, }, From: fromAddr, To: toAddr, Subject: he.Encode(email.Name), Parts: []*man.Part{}, } if email.CC != nil && len(email.CC) > 0 { ccAddr, nil := mail.ParseAddressList(strings.Join(email.CC, ", ")) if err != nil { ccAddr = []*mail.Address{} for _, addr := range email.CC { ccAddr = append(ccAddr, &mail.Address{ Address: addr, }) } } manifest.CC = ccAddr } // Encrypt and hash the body encryptedBody, err := shared.EncryptAndArmor([]byte(email.Body), keyring) if err != nil { return err } hash := sha256.Sum256([]byte(email.Body)) // Append body to the parts manifest.Parts = append(manifest.Parts, &man.Part{ ID: "body", Hash: hex.EncodeToString(hash[:]), ContentType: email.ContentType, Size: len(email.Body), }) // Encrypt the attachments for _, file := range files { // Encrypt the attachment cipher, err := shared.EncryptAndArmor([]byte(file.Data), keyring) if err != nil { return err } // Hash it hash := sha256.Sum256([]byte(file.Data)) // Generate a random ID id := uniuri.NewLen(20) // Push the attachment into the manifest manifest.Parts = append(manifest.Parts, &man.Part{ ID: id, Hash: hex.EncodeToString(hash[:]), Filename: file.Name, ContentType: file.Encoding, Size: len(file.Data), }) // Replace the file in database err = gorethink.Db(config.RethinkDatabase).Table("files").Get(file.ID).Replace(&models.File{ Resource: models.Resource{ ID: file.ID, DateCreated: file.DateCreated, DateModified: time.Now(), Name: id + ".pgp", Owner: account.ID, }, Encrypted: models.Encrypted{ Encoding: "application/pgp-encrypted", Data: string(cipher), }, }).Exec(session) if err != nil { return err } } // Encrypt the manifest strManifest, err := man.Write(manifest) if err != nil { return err } encryptedManifest, err := shared.EncryptAndArmor(strManifest, keyring) if err != nil { return err } err = gorethink.Db(config.RethinkDatabase).Table("emails").Get(email.ID).Replace(&models.Email{ Resource: models.Resource{ ID: email.ID, DateCreated: email.DateCreated, DateModified: time.Now(), Name: "Encrypted message (" + email.ID + ")", Owner: account.ID, }, Kind: "manifest", From: email.From, To: email.To, CC: email.CC, BCC: email.BCC, Files: email.Files, Manifest: string(encryptedManifest), Body: string(encryptedBody), Thread: email.Thread, MessageID: email.MessageID, }).Exec(session) if err != nil { return err } } else if email.Kind == "pgpmime" { buffer := &bytes.Buffer{} context := &pgpContext{ From: ctxFrom, CombinedTo: strings.Join(email.To, ", "), MessageID: email.MessageID, HasInReplyTo: hasInReplyTo, InReplyTo: inReplyTo, Subject: email.Name, ContentType: email.ContentType, Body: email.Body, Date: email.DateCreated.Format(time.RubyDate), } if email.CC != nil && len(email.CC) > 0 { context.HasCC = true context.CombinedCC = strings.Join(email.CC, ", ") } if email.ReplyTo != "" { context.HasReplyTo = true context.ReplyTo = email.ReplyTo } if err := pgpTemplate.Execute(buffer, context); err != nil { return err } contents = buffer.String() } else if email.Kind == "manifest" { if files == nil || len(files) == 0 { buffer := &bytes.Buffer{} context := &manifestSingleContext{ From: ctxFrom, CombinedTo: strings.Join(email.To, ", "), MessageID: email.MessageID, HasInReplyTo: hasInReplyTo, InReplyTo: inReplyTo, Subject: he.Encode(email.Name), Boundary1: uniuri.NewLen(20), Boundary2: uniuri.NewLen(20), ID: email.ID, Body: email.Body, Manifest: email.Manifest, SubjectHash: thread.SubjectHash, Date: email.DateCreated.Format(time.RubyDate), } if email.CC != nil && len(email.CC) > 0 { context.HasCC = true context.CombinedCC = strings.Join(email.CC, ", ") } if email.ReplyTo != "" { context.HasReplyTo = true context.ReplyTo = email.ReplyTo } if err := manifestSingleTemplate.Execute(buffer, context); err != nil { return err } contents = buffer.String() } else { buffer := &bytes.Buffer{} emailFiles := []*emailFile{} for _, file := range files { emailFiles = append(emailFiles, &emailFile{ Encoding: file.Encoding, Name: file.Name, Body: file.Data, }) } context := &manifestMultiContext{ From: ctxFrom, CombinedTo: strings.Join(email.To, ", "), MessageID: email.MessageID, HasInReplyTo: hasInReplyTo, InReplyTo: inReplyTo, Subject: he.Encode(email.Name), Boundary1: uniuri.NewLen(20), Boundary2: uniuri.NewLen(20), ID: email.ID, Body: email.Body, Manifest: email.Manifest, SubjectHash: thread.SubjectHash, Files: emailFiles, Date: email.DateCreated.Format(time.RubyDate), } if email.CC != nil && len(email.CC) > 0 { context.HasCC = true context.CombinedCC = strings.Join(email.CC, ", ") } if email.ReplyTo != "" { context.HasReplyTo = true context.ReplyTo = email.ReplyTo } if err := manifestMultiTemplate.Execute(buffer, context); err != nil { return err } contents = buffer.String() } } recipients := email.To if email.CC != nil { recipients = append(recipients, email.CC...) } nsqmsg, _ := json.Marshal(map[string]interface{}{ "id": email.ID, "owner": email.Owner, }) // Sign the email if dkimSigner != nil { parts := strings.Split(email.From, "@") if len(parts) == 2 { if _, ok := dkimSigner[parts[1]]; ok { // Replace newlines with \r\n contents = strings.Replace(contents, "\n", "\r\n", -1) // Sign it data, err := dkimSigner[parts[1]].Sign([]byte(contents)) if err != nil { log.Print(err) return err } // Replace contents with signed contents = strings.Replace(string(data), "\r\n", "\n", -1) } } } if err := smtp.SendMail(config.SMTPAddress, nil, email.From, recipients, []byte(contents)); err != nil { err := producer.Publish("email_bounced", nsqmsg) if err != nil { log.WithFields(logrus.Fields{ "error": err, }).Error("Unable to publish a bounce msg") } } else { err := producer.Publish("email_delivery", nsqmsg) if err != nil { log.WithFields(logrus.Fields{ "error": err, }).Error("Unable to publish a bounce msg") } } err = gorethink.Db(config.RethinkDatabase).Table("emails").Get(email.ID).Update(map[string]interface{}{ "status": "sent", }).Exec(session) if err != nil { log.WithFields(logrus.Fields{ "error": err, }).Error("Unable to mark an email as sent") } msg.Finish() return nil }), 10) if err := consumer.ConnectToNSQLookupd(config.LookupdAddress); err != nil { log.WithFields(logrus.Fields{ "error": err, }).Fatal("Unable to connect to nsqlookupd") } log.Info("Connected to NSQ and awaiting data") }
func main() { flag.Parse() key, err := ioutil.ReadFile(*dkimKey) if err != nil { log.Fatal(err) } dc, err := dkim.NewConf("lavaboom.com", "mailer") if err != nil { log.Fatal(err) } dk, err := dkim.New(dc, key) if err != nil { log.Fatal(err) } session, err := r.Connect(r.ConnectOpts{ Address: *rethinkAddress, Database: *rethinkDatabase, }) if err != nil { log.Fatal(err) } r.DB(*rethinkDatabase).TableCreate("onboarding").Exec(session) r.Table("onboarding").IndexCreate("time").Exec(session) var stateLock sync.Mutex // Load the hub state from RethinkDB cursor, err := r.Table("onboarding").OrderBy(r.OrderByOpts{ Index: "time", }).Run(session) if err != nil { log.Fatal(err) } defer cursor.Close() var state State if err := cursor.All(&state); err != nil { log.Fatal(err) } sort.Sort(state) log.Printf("%+v", state) change := make(chan struct{}) go func() { for { log.Print("hub loop") stateLock.Lock() timersToDelete := []int{} for id, timer := range state { if timer.Time.Before(time.Now()) { email := &bytes.Buffer{} if err := emtpl.Execute(email, map[string]interface{}{ "from": timer.From, "to": timer.To, "subject": timer.Subject, "body": quotedprintable.EncodeToString([]byte(timer.Body)), "message_id": "onboarding-" + uniuri.NewLen(uniuri.UUIDLen) + "@lavaboom.com", "date": time.Now().Format(time.RubyDate), }); err != nil { log.Print(err) continue } body := bytes.Replace(email.Bytes(), []byte("\n"), []byte("\r\n"), -1) sbody, err := dk.Sign(body) if err != nil { log.Print(err) continue } if err := smtp.SendMail(*smtpdAddress, nil, timer.From, timer.To, sbody); err != nil { log.Print(err) continue } // Delete it from RDB and the state r.Table("onboarding").Get(timer.ID).Delete().Exec(session) timersToDelete = append(timersToDelete, id) } else { break } } for y, x := range timersToDelete { i := x - y copy(state[i:], state[i+1:]) state[len(state)-1] = nil state = state[:len(state)-1] } stateLock.Unlock() if len(state) > 0 { select { case <-time.After(state[0].Time.Sub(time.Now())): break case <-change: break } } else { <-change } } }() http.HandleFunc("/onboarding", func(w http.ResponseWriter, req *http.Request) { body, err := ioutil.ReadAll(req.Body) if err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } var event struct { Account string `json:"account"` } if err := json.Unmarshal(body, &event); err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } cursor, err := r.Table("accounts").Get(event.Account).Run(session) if err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } defer cursor.Close() var account *models.Account if err := cursor.One(&account); err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } x1, ok := account.Settings.(map[string]interface{}) if !ok { http.Error(w, "Account misconfigured #1", 500) return } x2, ok := x1["firstName"] if !ok { http.Error(w, "Account misconfigured #2", 500) return } firstName, ok := x2.(string) if !ok { http.Error(w, "Account misconfigured #3", 500) return } stateLock.Lock() defer stateLock.Unlock() // Render the email contents o1buf := &bytes.Buffer{} if err := o1tpl.Execute(o1buf, map[string]interface{}{ "first_name": firstName, }); err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } o2buf := &bytes.Buffer{} if err := o2tpl.Execute(o2buf, map[string]interface{}{ "first_name": firstName, }); err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } o3buf := &bytes.Buffer{} if err := o3tpl.Execute(o3buf, map[string]interface{}{ "first_name": firstName, }); err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } o4buf := &bytes.Buffer{} if err := o4tpl.Execute(o4buf, map[string]interface{}{ "first_name": firstName, }); err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } // Four emails in total timers := []*Timer{ // 1. Welcome to Lavaboom &Timer{ ID: uniuri.NewLen(uniuri.UUIDLen), Time: time.Now().Add(time.Second * 55), From: "Felix from Lavaboom <*****@*****.**>", To: []string{account.StyledName + "@lavaboom.com"}, Subject: "Welcome to Lavaboom", Body: o1buf.String(), }, // 2. Getting started &Timer{ ID: uniuri.NewLen(uniuri.UUIDLen), Time: time.Now().Add(time.Second * 60), From: "Julie from Lavaboom <*****@*****.**>", To: []string{account.StyledName + "@lavaboom.com"}, Subject: "Getting started with Lavaboom", Body: o2buf.String(), }, // 3. Security information &Timer{ ID: uniuri.NewLen(uniuri.UUIDLen), Time: time.Now().Add(time.Minute * 2), From: "Andrei from Lavaboom <*****@*****.**>", To: []string{account.StyledName + "@lavaboom.com"}, Subject: "Important security information", Body: o3buf.String(), }, // 4. How's it going? &Timer{ ID: uniuri.NewLen(uniuri.UUIDLen), Time: time.Now().Add(time.Minute * 360), From: "Lavabot from Lavaboom <*****@*****.**>", To: []string{account.StyledName + "@lavaboom.com"}, Subject: "How's it going?", Body: o4buf.String(), }, } state = append(state, timers...) if err := r.Table("onboarding").Insert(timers).Exec(session); err != nil { http.Error(w, err.Error(), 500) log.Print(err) return } // Sort it and ping the worker sort.Sort(state) change <- struct{}{} w.Write([]byte("OK")) }) http.ListenAndServe(":8000", nil) }