160 lines
5.9 KiB
Go
160 lines
5.9 KiB
Go
package controller
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/ente-io/museum/ente"
|
|
"github.com/ente-io/museum/pkg/external/zoho"
|
|
"github.com/ente-io/stacktrace"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
// MailingListsController is used to keeping the external mailing lists in sync
|
|
// with customer email changes.
|
|
//
|
|
// MailingListsController contains methods for keeping external mailing lists in
|
|
// sync when new users sign up, or update their email, or delete their account.
|
|
// Currently, these mailing lists are hosted on Zoho Campaigns.
|
|
//
|
|
// See also: Syncing emails with Zoho Campaigns
|
|
type MailingListsController struct {
|
|
zohoAccessToken string
|
|
zohoListKey string
|
|
zohoTopicIds string
|
|
zohoCredentials zoho.Credentials
|
|
}
|
|
|
|
// Return a new instance of MailingListsController
|
|
func NewMailingListsController() *MailingListsController {
|
|
zohoCredentials := zoho.Credentials{
|
|
ClientID: viper.GetString("zoho.client-id"),
|
|
ClientSecret: viper.GetString("zoho.client-secret"),
|
|
RefreshToken: viper.GetString("zoho.refresh-token"),
|
|
}
|
|
|
|
// The Zoho "List Key" identifies a particular list of email IDs that are
|
|
// stored in Zoho. All the actions that we perform (adding, removing and
|
|
// updating emails) are done on this list.
|
|
//
|
|
// https://www.zoho.com/campaigns/help/developers/list-management.html
|
|
zohoListKey := viper.GetString("zoho.list-key")
|
|
|
|
// List of topics to which emails are sent.
|
|
//
|
|
// Ostensibly, we can get them from their API
|
|
// https://www.zoho.com/campaigns/oldhelp/api/get-topics.html
|
|
//
|
|
// But that doesn't currently work, luckily we can get these IDs by looking
|
|
// at the HTML source of the topic update dashboard page.
|
|
zohoTopicIds := viper.GetString("zoho.topic-ids")
|
|
|
|
// Zoho has a rate limit on the number of access tokens that can created
|
|
// within a given time period. So as an aid in debugging, allow the access
|
|
// token to be passed in. This will not be present in production - there
|
|
// we'll use the refresh token to create an access token on demand.
|
|
zohoAccessToken := viper.GetString("zoho.access_token")
|
|
|
|
return &MailingListsController{
|
|
zohoCredentials: zohoCredentials,
|
|
zohoListKey: zohoListKey,
|
|
zohoTopicIds: zohoTopicIds,
|
|
zohoAccessToken: zohoAccessToken,
|
|
}
|
|
}
|
|
|
|
// Add the given email address to our default Zoho Campaigns list.
|
|
//
|
|
// It is valid to resubscribe an email that has previously been unsubscribe.
|
|
//
|
|
// # Syncing emails with Zoho Campaigns
|
|
//
|
|
// Zoho Campaigns does not support maintaining a list of raw email addresses
|
|
// that can be later updated or deleted via their API. So instead, we maintain
|
|
// the email addresses of our customers in a Zoho Campaign "list", and subscribe
|
|
// or unsubscribe them to this list.
|
|
func (c *MailingListsController) Subscribe(email string) error {
|
|
if c.shouldSkip() {
|
|
return stacktrace.Propagate(ente.ErrNotImplemented, "")
|
|
}
|
|
|
|
// Need to set "Signup Form Disabled" in the list settings since we use this
|
|
// list to keep track of emails that have already been verified.
|
|
//
|
|
// > You can use this API to add contacts to your mailing lists. For signup
|
|
// form enabled mailing lists, the contacts will receive a confirmation
|
|
// email. For signup form disabled lists, contacts will be added without
|
|
// any confirmations.
|
|
//
|
|
// https://www.zoho.com/campaigns/help/developers/contact-subscribe.html
|
|
return c.doListAction("listsubscribe", email)
|
|
}
|
|
|
|
// Unsubscribe the given email address to our default Zoho Campaigns list.
|
|
//
|
|
// See: [Note: Syncing emails with Zoho Campaigns]
|
|
func (c *MailingListsController) Unsubscribe(email string) error {
|
|
if c.shouldSkip() {
|
|
return stacktrace.Propagate(ente.ErrNotImplemented, "")
|
|
}
|
|
|
|
// https://www.zoho.com/campaigns/help/developers/contact-unsubscribe.html
|
|
return c.doListAction("listunsubscribe", email)
|
|
}
|
|
|
|
func (c *MailingListsController) shouldSkip() bool {
|
|
if c.zohoCredentials.RefreshToken == "" {
|
|
log.Info("Skipping mailing list update because credentials are not configured")
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Both the listsubscribe and listunsubscribe Zoho Campaigns API endpoints work
|
|
// similarly, so use this function to keep the common code.
|
|
func (c *MailingListsController) doListAction(action string, email string) error {
|
|
// Query escape the email so that any pluses get converted to %2B.
|
|
escapedEmail := url.QueryEscape(email)
|
|
contactInfo := fmt.Sprintf("{Contact+Email: \"%s\"}", escapedEmail)
|
|
// Instead of using QueryEscape, use PathEscape. QueryEscape escapes the "+"
|
|
// character, which causes Zoho API to not recognize the parameter.
|
|
escapedContactInfo := url.PathEscape(contactInfo)
|
|
|
|
url := fmt.Sprintf(
|
|
"https://campaigns.zoho.com/api/v1.1/json/%s?resfmt=JSON&listkey=%s&contactinfo=%s&topic_id=%s",
|
|
action, c.zohoListKey, escapedContactInfo, c.zohoTopicIds)
|
|
|
|
zohoAccessToken, err := zoho.DoRequest("POST", url, c.zohoAccessToken, c.zohoCredentials)
|
|
c.zohoAccessToken = zohoAccessToken
|
|
|
|
if err != nil {
|
|
// This is not necessarily an error, and can happen when the customer
|
|
// had earlier unsubscribed from our organization emails in Zoho,
|
|
// selecting the "Erase my data" option. This causes Zoho to remove the
|
|
// customer's entire record from their database.
|
|
//
|
|
// Then later, say if the customer deletes their account from ente, we
|
|
// would try to unsubscribe their email but it wouldn't be present in
|
|
// Zoho, and this API call would've failed.
|
|
//
|
|
// In such a case, Zoho will return the following response:
|
|
//
|
|
// { code":"2103",
|
|
// "message":"Contact does not exist.",
|
|
// "version":"1.1",
|
|
// "uri":"/api/v1.1/json/listunsubscribe",
|
|
// "status":"error"}
|
|
//
|
|
// Special case these to reduce the severity level so as to not cause
|
|
// error log spam.
|
|
if strings.Contains(err.Error(), "Contact does not exist") {
|
|
log.Warnf("Zoho - Could not %s '%s': %s", action, email, err)
|
|
} else {
|
|
log.Errorf("Zoho - Could not %s '%s': %s", action, email, err)
|
|
}
|
|
}
|
|
|
|
return stacktrace.Propagate(err, "")
|
|
}
|