Beispiel #1
0
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")
}
Beispiel #2
0
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)
}
Beispiel #3
0
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)
}