From 598f63e16e6ad61171610bc21cfedb5ae58b1316 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Thu, 22 Jan 2015 21:56:54 +0100 Subject: [PATCH 1/3] Email encryption code --- models/account.go | 4 +- models/email.go | 2 +- routes/accounts.go | 2 +- routes/emails.go | 152 ++++++++++++++++++++++++++++++++++++++++++--- routes/keys.go | 4 +- 5 files changed, 149 insertions(+), 15 deletions(-) diff --git a/models/account.go b/models/account.go index 6159ae0..02b39b6 100644 --- a/models/account.go +++ b/models/account.go @@ -18,8 +18,8 @@ type Account struct { // It's hashed and salted using scrypt. Password string `json:"-" gorethink:"password"` - // PGPKey is the fingerprint of account's default key - PGPKey string `json:"pgp_key" gorethink:"pgp_key"` + // PublicKey is the fingerprint of account's default key + PublicKey string `json:"public_key" gorethink:"public_key"` // Settings contains data needed to customize the user experience. Settings interface{} `json:"settings" gorethink:"settings"` diff --git a/models/email.go b/models/email.go index 634c416..206536a 100644 --- a/models/email.go +++ b/models/email.go @@ -29,7 +29,7 @@ type Email struct { // Headers []string // Body string // Snippet string - Preview Encrypted `json:"preview" gorethink:"preview"` + //Preview Encrypted `json:"preview" gorethink:"preview"` // ThreadID Thread string `json:"thread" gorethink:"thread"` diff --git a/routes/accounts.go b/routes/accounts.go index 13a1da7..bb535bb 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -595,7 +595,7 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { return } - user.PGPKey = input.PublicKey + user.PublicKey = input.PublicKey } if input.FactorType != "" { diff --git a/routes/emails.go b/routes/emails.go index 9b53d6b..ef69df8 100644 --- a/routes/emails.go +++ b/routes/emails.go @@ -1,12 +1,16 @@ package routes import ( + "bytes" + "io" "net/http" "strconv" "strings" "github.com/Sirupsen/logrus" "github.com/zenazn/goji/web" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" "github.com/lavab/api/env" "github.com/lavab/api/models" @@ -264,18 +268,10 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { Encoding: "json", PGPFingerprints: input.PGPFingerprints, Data: input.Body, - Schema: "email_body", + Schema: "email", VersionMajor: input.BodyVersionMajor, VersionMinor: input.BodyVersionMinor, }, - Preview: models.Encrypted{ - Encoding: "json", - PGPFingerprints: input.PGPFingerprints, - Data: input.Preview, - Schema: "email_preview", - VersionMajor: input.PreviewVersionMajor, - VersionMinor: input.PreviewVersionMinor, - }, Status: "queued", } @@ -292,6 +288,32 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { return } + // I'm going to whine at this part, as we are doubling the email sending code + + // Check if To contains lavaboom emails + for _, address := range email.To { + parts := strings.SplitN(address, "@", 2) + if parts[1] == env.Config.EmailDomain { + go sendEmail(parts[0], email) + } + } + + // Check if CC contains lavaboom emails + for _, address := range email.CC { + parts := strings.SplitN(address, "@", 2) + if parts[1] == env.Config.EmailDomain { + go sendEmail(parts[0], email) + } + } + + // Check if BCC contains lavaboom emails + for _, address := range email.BCC { + parts := strings.SplitN(address, "@", 2) + if parts[1] == env.Config.EmailDomain { + go sendEmail(parts[0], email) + } + } + // Add a send request to the queue err = env.NATS.Publish("send", email.ID) if err != nil { @@ -401,3 +423,115 @@ func EmailsDelete(c web.C, w http.ResponseWriter, r *http.Request) { Message: "Email successfully removed", }) } + +func sendEmail(account string, email *models.Email) { + // find recipient's account + recipient, err := env.Accounts.FindAccountByName(account) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err.Error(), + "name": account, + }).Warn("Unable to fetch recipent's account") + return + } + + newEmail := *email + + // check if the email is unencrypted + if newEmail.Body.PGPFingerprints == nil || len(newEmail.Body.PGPFingerprints) == 0 { + // check if the acc has a pkey set + if recipient.PublicKey == "" { + env.Log.WithFields(logrus.Fields{ + "name": account, + }).Warn("Recipient has no public key set") + return + } + + // fetch the pkey + key, err := env.Keys.FindByFingerprint(recipient.PublicKey) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err.Error(), + "fingerprint": recipient.PublicKey, + "name": account, + }).Warn("Recipient's public key does not exist") + return + } + + // parse the armored key + entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(key.Key)) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err.Error(), + "fingerprint": recipient.PublicKey, + }).Warn("Cannot parse an armored key") + return + } + + // first key should be the pkey + publicKey := entityList[0] + + // prepare a buffer for ciphertext and initialize openpgp + output := &bytes.Buffer{} + input, err := openpgp.Encrypt(output, []*openpgp.Entity{publicKey}, nil, nil, nil) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err.Error(), + "fingerprint": recipient.PublicKey, + }).Warn("Cannot set up an OpenPGP encrypter") + return + } + + // write email's contents into input + _, err = input.Write([]byte(newEmail.Body.Data)) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err.Error(), + "fingerprint": recipient.PublicKey, + }).Warn("Cannot write into the OpenPGP's input") + return + } + + // close the input + if err := input.Close(); err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err.Error(), + "fingerprint": recipient.PublicKey, + }).Warn("Cannot close OpenPGP's input") + return + } + + // encode output into armor + armoredOutput := &bytes.Buffer{} + armoredInput, err := armor.Encode(armoredOutput, "PGP MESSAGE", map[string]string{ + "Version": "Lavaboom " + env.Config.APIVersion, + }) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err.Error(), + }).Warn("Cannot initialize a new armor encoding") + return + } + + _, err = io.Copy(armoredInput, output) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err.Error(), + }).Warn("Unable to copy encrypted ciphertext into the armor processor") + return + } + + if err := armoredInput.Close(); err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err.Error(), + "fingerprint": recipient.PublicKey, + }).Warn("Cannot close armoring's input") + return + } + + newEmail.Body.PGPFingerprints = []string{recipient.PublicKey} + newEmail.Body.Data = armoredOutput.String() + } + + // +} diff --git a/routes/keys.go b/routes/keys.go index 873cab3..fc3f89b 100644 --- a/routes/keys.go +++ b/routes/keys.go @@ -230,9 +230,9 @@ func KeysGet(c web.C, w http.ResponseWriter, r *http.Request) { } // Does the user have a default PGP key set? - if account.PGPKey != "" { + if account.PublicKey != "" { // Fetch the requested key from the database - key2, err := env.Keys.FindByFingerprint(account.PGPKey) + key2, err := env.Keys.FindByFingerprint(account.PublicKey) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err.Error(), From 85d58db605f64e3db2163967e2c10152399956e5 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 23 Jan 2015 00:00:44 +0100 Subject: [PATCH 2/3] untested internal messaging --- routes/emails.go | 82 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/routes/emails.go b/routes/emails.go index ef69df8..0c70c6d 100644 --- a/routes/emails.go +++ b/routes/emails.go @@ -4,6 +4,7 @@ import ( "bytes" "io" "net/http" + "regexp" "strconv" "strings" @@ -17,6 +18,8 @@ import ( "github.com/lavab/api/utils" ) +var prefixesRegex = regexp.MustCompile(`([\[\(] *)?(RE?S?|FYI|RIF|I|FS|VB|RV|ENC|ODP|PD|YNT|ILT|SV|VS|VL|AW|WG|ΑΠ|ΣΧΕΤ|ΠΡΘ|תגובה|הועבר|主题|转发|FWD?) *([-:;)\]][ :;\])-]*|$)|\]+ *$`) + // EmailsListResponse contains the result of the EmailsList request. type EmailsListResponse struct { Success bool `json:"success"` @@ -533,5 +536,82 @@ func sendEmail(account string, email *models.Email) { newEmail.Body.Data = armoredOutput.String() } - // + // Get the "Inbox" label's ID + var inbox *models.Label + err = env.Labels.WhereAndFetchOne(map[string]interface{}{ + "name": "Inbox", + "builtin": true, + "owner": recipient.ID, + }, &inbox) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "id": recipient.ID, + "error": err.Error(), + }).Warn("Account has no inbox label") + return + } + + // strip prefixes from the subject + rawSubject := prefixesRegex.ReplaceAllString(newEmail.Name, "") + + emailResource := models.MakeResource(recipient.ID, newEmail.Name) + + var thread *models.Thread + err = env.Threads.WhereAndFetchOne(map[string]interface{}{ + "name": rawSubject, + "owner": recipient.ID, + }, &thread) + if err != nil { + thread = &models.Thread{ + Resource: models.MakeResource(recipient.ID, rawSubject), + Emails: []string{emailResource.ID}, + Labels: []string{inbox.ID}, + Members: append(append(newEmail.To, newEmail.CC...), newEmail.BCC...), + IsRead: false, + } + + err := env.Threads.Insert(thread) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err.Error(), + }).Error("Unable to create a new thread") + return + } + } + + // Insert the new email + newEmail.Resource = emailResource + newEmail.Status = "processed" + newEmail.Thread = thread.ID + + err = env.Emails.Insert(newEmail) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err.Error(), + }).Error("Unable to create a new email") + return + } + + // Send notifications + err = env.NATS.Publish("delivery", map[string]interface{}{ + "id": email.ID, + "owner": email.Owner, + }) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "id": email.ID, + "error": err.Error(), + }).Error("Unable to publish a delivery message") + } + + err = env.NATS.Publish("receipt", map[string]interface{}{ + "id": newEmail.ID, + "owner": newEmail.Owner, + }) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "id": newEmail.ID, + "error": err.Error(), + }).Error("Unable to publish a receipt message") + } } From 46848ec068a182083d5b2be062fe3e93851808b6 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 23 Jan 2015 00:25:31 +0100 Subject: [PATCH 3/3] Complete internal messages --- routes/emails.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/routes/emails.go b/routes/emails.go index 0c70c6d..def605d 100644 --- a/routes/emails.go +++ b/routes/emails.go @@ -12,6 +12,7 @@ import ( "github.com/zenazn/goji/web" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/armor" + _ "golang.org/x/crypto/ripemd160" "github.com/lavab/api/env" "github.com/lavab/api/models" @@ -577,6 +578,42 @@ func sendEmail(account string, email *models.Email) { }).Error("Unable to create a new thread") return } + } else { + existingMembers := make(map[string]struct{}) + + for _, member := range thread.Members { + existingMembers[member] = struct{}{} + } + + for _, member := range newEmail.To { + if _, ok := existingMembers[member]; !ok { + thread.Members = append(thread.Members, member) + existingMembers[member] = struct{}{} + } + } + + for _, member := range newEmail.CC { + if _, ok := existingMembers[member]; !ok { + thread.Members = append(thread.Members, member) + existingMembers[member] = struct{}{} + } + } + + for _, member := range newEmail.BCC { + if _, ok := existingMembers[member]; !ok { + thread.Members = append(thread.Members, member) + existingMembers[member] = struct{}{} + } + } + + err := env.Threads.UpdateID(thread.ID, thread) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "id": thread.ID, + "error": err.Error(), + }).Error("Unable to update an existing thread") + return + } } // Insert the new email