Home Blog Hoe migreer je van Mailchimp naar Postmark + Temporal

Stuur een mail op een typemachine Foto door Markus Winkler

Hoe migreer je van Mailchimp naar Postmark + Temporal

MailChimp is een geweldig SaaS platform om e-mails naar je abonnees te sturen en met automatisering te werken om aan je mailinglijst te verkopen. Maar als je eenmaal een omvangrijk publiek hebt en je e-mail interacties wilt aanpassen, krijg je te maken met een fors prijskaartje. Mailchimp is erg duur! Als je een mailinglijst hebt van 2000 contacten en je zit op het goedkoopste betaalde plan, dan betaal je al $34 per maand. Dit is een prima optie voor een marketing team dat geen toegang heeft tot een software ontwikkelaar, maar ik ben zo'n software ontwikkelaar.

Voor we met de migratie beginnen, stellen we de 2 helden van het verhaal voor: Postmark en Temporal. Als je nog niet eerder van deze twee gehoord hebt, volgt hier een korte introductie. Als je ze al wel kent, kun je dit overslaan en naar de delen gaan die je wilt lezen:

  1. Wat is Postmark?
  2. Wat is Temporal?
  3. Maak e-mail sjablonen in Postmark
  4. Maak Temporele werkstromen
    1. De nieuwsbrief verstuur-workflow
    2. Behandel afmeldingen voor de nieuwsbrief

Wat is Postmark?

Postmark homepage

Misschien heb je al eens van Postmark gehoord, het is een vrij bekend e-mail platform waarmee je transactionele- en marketing e-mails kunt versturen en zelfs inkomende e-mails kunt afhandelen. In mijn ervaring is het leveringspercentage beter dan dat van een paar andere opties die er zijn. Een van de grootste voordelen is het feit dat je templates in code kunt maken en die met Postmark kunt synchroniseren. Dit is een heel belangrijk aspect voor dit hele proces, want hiermee kan ik e-mails versturen met behulp van een REST API.

Wat is Temporal?

Temporal homepage

Het tweede stukje software dat dit alles mogelijk maakt is Temporal. Temporal is software die de state van je code bijhoudt. Eenvoudig gezegd voert het elke regel code slechts eenmaal uit en kan het de uitvoering van code hervatten na een systeemcrash. Dit maakt je code ongelooflijk stabiel. Als dat je nog niet overtuigd heeft van de kracht ervan, kan het volgende voorbeeld het misschien verduidelijken.

Voorbeeld

Als je een bankprogramma hebt en je klant stuurt een betaling naar een andere klant en je programma crasht halverwege: wat gebeurde er met de betaling? Wat gebeurt er als je de applicatie opnieuw start? Zal dit proces 2 keer geld van je klant afschrijven? Temporal weet waar de applicatie was in zijn code-uitvoering en hervat vanaf die exacte regel. Temporal helpt je om je klant nooit twee keer te laten betalen, zelfs niet tijdens een crash.

Laat je code weken wachten

Een ander groot voordeel van Temporal is dat je processen weken of maanden kunt laten wachten als je dat wilt. In normale code kun je een soort vertraging (sleep, time.Sleep, enz.) in je code zetten, maar je zou er niet aan denken om dit voor 14 dagen te doen. Meestal zou je je code maar met ongeveer 10 seconden vertragen.

Maar waarom is dit belangrijk? Dit is belangrijk, want het vereenvoudigt je code drastisch. Een klant voor een abonnement laten betalen is nu gewoon een for-loop met een vertraging van 30 dagen na de betaling. Controleer 30 dagen later of de klant nog geabonneerd is, reken af, en wacht nog eens 30 dagen. Het is echt zo eenvoudig. Houd dit in je achterhoofd, want dit zal heel belangrijk worden bij dit inplannen van e-mails om naar je contacten te sturen.

Nog een laatste ding om te benadrukken voor we kunnen beginnen! Je kunt de uitvoering van code plannen op basis van een cron schema. Dit wordt het hart van de nieuwsbrief applicatie. Genoeg gepraat, laten we aan wat code beginnen!

Maak e-mail templates in Postmark

Als je ooit e-mails vanuit je code gestuurd hebt, heb je misschien met lokale templates gewerkt en kant-en-klare HTML e-mails naar je contacten gestuurd. Dit is altijd een pijnpunt en ik kan niet eens beginnen te tellen hoe vaak een e-mail er op een mobiel toestel net niet goed uitziet of er op Gmail in Safari een beetje naast zit. Het maken van e-mails in HTML brengt je gewoon terug naar de "Good ol' HTML 4 dagen" waarin alles een tabel is en niets responsief is. Laten we niet eens proberen dit te doen en laat Postmark dit voor je afhandelen.

Als je de mailmason starter gebruikt die ik ook als basis heb gebruikt, zie je minimale HTML tabellen en meestal alleen eenvoudige HTML templates. Je kunt je e-mails stijlen met eenvoudige CSS bestanden en Mailmason genereert zelfs de platte tekst versies van je e-mails.

Nadat je je templates met je Postmark server gesynchroniseerd hebt, heb je templates met gemakkelijk te gebruiken aliassen. Je kunt deze template aliassen gebruiken, naast de variabelen die je voor je templates nodig hebt (voornaam, enz.) en ze als JSON naar Postmark sturen om je e-mail te versturen. E-mails versturen met een eenvoudige REST API aanroep is een fantastisch gevoel.

Laten we ons ten behoeve van deze post voorstellen dat we een template als dit gebruiken:

<h1>Hallo!</h1>
<p>
    Ik hoop dat je een fijne week hebt gehad! Dit is wat ik deze week voor je geschreven heb, ik hoop dat je ervan geniet!
</p>

<img src="{{image_url}}" >

<br />

<h1>{{title}}</h1>

<p>{{description}}</p>

<a href="{{url}}" class="cta-button">Lees &quot;{{title}}&quot;</a>

<p>
    Ik kijk ernaar uit je binnenkort de volgende e-mail te sturen, doei!
</p>

Zoals je ziet, hebben we een paar variabelen die we kunnen invullen met de REST API:

  • image_url
  • title
  • description
  • url

Laten we deze template het alias "weekly-newsletter" geven.

Dit is een heel eenvoudige e-mail template met minimale styling dat op de Postmark servers wordt opgeslagen, in plaats van in je eigen applicatie.

Als je geïnteresseerd bent in meer informatie over mailmason en mijn automatische proces om templates met Postmark te synchroniseren, neem dan contact met me op.

Laten we overgaan tot het hart van de automatisering: Temporal.

Maak Temporal-workflows

Temporal, de state machine, heeft een concept dat workflows heet. Een workflow is een verzameling afzonderlijke stappen die een (meer ingewikkelde) taak uitvoeren. Een workflow moet deterministisch zijn, wat betekent dat het telkens hetzelfde resultaat heeft als het uitgevoerd wordt met dezelfde input.

Elk niet-deterministisch gedrag moet naar een activity verplaatst worden, want het resultaat van een activity wordt in de geschiedenis van de workflow vastgelegd. Je kunt een activity interpreteren als een stap in je workflow. Enkele voorbeelden van code die goed is voor een activity is: een e-mail sturen of iets ophalen uit een API.

Als we dit opsplitsen voor het versturen van een nieuwsbrief, kun je een workflow zien als deze stappen:

  1. Naar wie stuur ik een e-mail?
  2. Is er iets voor mij om te sturen? Zo nee, kijk dan volgende week nog eens.
  3. Zeg Postmark dat het contact X template Y met variabelen Z moet sturen
  4. Klaar, controleer volgende week opnieuw

Tijdens deze workflow wil ik de code zo snel mogelijk overslaan en volgende week opnieuw controleren. "Volgende week weer controleren" belicht een van de mogelijkheden die Temporal biedt voor workflows: Cronjob workflows. Deze Cronjob workflows kunnen uitgevoerd worden wanneer je maar wilt, in mijn geval elke zaterdag om 15:00 (15 uur). Dit cron schema ziet er als volgt uit: "0 15 * * 6".

Het starten (inplannen) van een workflow in mijn code, die in Go geschreven is, ziet er als volgt uit:

// Maak een eenvoudig te gebruiken workflow ID
const WorkflowID = "nieuwsbrief-%s"

// Registreer de workflow met een makkelijke naam
w.RegisterWorkflowWithOptions(EmailNewsletterWorkflow, workflow.RegisterOptions{
    Name: "email.nieuwsbrief",
})

type SubscribeRequest struct {
    Email string `json: "email"`
}

// Voer de workflow uit met een cron schema
als _, err := s.client.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
    ID: fmt.Sprintf(WorkflowID, "hello@example.com"),
    TaskQueue: Queue,
    CronSchedule: "0 15 * * 6",
}, "email.nieuwsbrief", SubscribeRequest{
    Email: "hello@example.com",
}); err != nil {
    return err
}

Er is veel code die ik in dit voorbeeld weglaat, zoals het opzetten van een Temporal worker en het instellen van de Temporal client. Om het eenvoudig te houden hardcode ik in dit voorbeeld ook het e-mail adres. De invoergegevens voor de workflow, "SubscribeRequest", hebben json tags, omdat temporal deze invoergegevens in de database opslaat en door de keys die het moet gebruiken als json te specificeren, vermijd je enkele zeldzame codeer- en decodeerproblemen.

Het workflow ID

Ik leg de nadruk op het gemakkelijk te gebruiken Workflow ID, omdat we zo de workflow gemakkelijk kunnen stoppen voor het geval het contact zich afmeldt van de mailinglijst. Door dit workflow ID te specificeren voorkom je ook dat de workflow meerdere keren loopt, voor het geval iemand zich per ongeluk (of expres) meerdere keren op je mailinglijst inschrijft. Als je geen workflow ID opgeeft, wordt er een willekeurige UUID aan toegekend, wat het erg moeilijk maakt de workflow te annuleren zonder dit willekeurige workflow ID in een andere database op te slaan.

De nieuwsbrief workflow

We hebben gezien dat we een workflow kunnen uitvoeren op basis van een cron schema, maar de workflow doet nog niets. Laten we daar verandering in brengen! In de workflow moeten we 2 dingen doen:

  1. Haal de post op dat we onze abonnee willen sturen
  2. Stuur de e-mail

Deze workflow zou er ongeveer zo uit kunnen zien:

type Post struct {
    Title         string `json:"title"`
    Image         string `json:"image"`
    Description   string `json:"description"`
    URL           string `json:"url"`
    PostedDaysAgo int    `json:"posted_days_ago"`
}

w.RegisterActivityWithOptions(FetchLatestPost, activity.RegisterOptions{
    Naam: "post.fetch-latest",
})

w.RegistreerActiviteitMetOpties(SendPostmarkTemplate, activiteit.RegisterOpties{
    Naam: "email.send",
})

func EmailNewsletterWorkflow(ctx workflow.Context, config SubscribeRequest) error {

    // We willen beide activiteiten maximaal 10 keer opnieuw proberen.
    ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
        TaskQueue:           Queue,
        StartToCloseTimeout: time.Minute,
        RetryPolicy: &temporal.RetryPolicy{
            InitialInterval:    time.Second,
            BackoffCoefficient: 2.0,
            MaximumInterval:    time.Minute,
            MaximumAttempts:    10,
        },
    })

    var latestPost Post

    // Roep een API endpoint binnen een activiteit aan om de laatste post op te halen
    if err := workflow.
        ExecuteActivity(ctx, "post.fetch-latest").
        Get(ctx, &latestPost); err != nil {
        return err
    }

    // We willen voorkomen dat we e-mails sturen als er de laatste 7 dagen geen nieuwe post was 
    if latestPost.PostedDaysAgo > 7 {
        workflow.GetLogger(ctx).Info("latest post is posted more than 7 days ago, skipping...")
        return nil
    }

    if err := workflow.
        ExecuteActivity(ctx, "email.send", Config{
            TemplateAlias: "weekly-newsletter",
            Email:         config.Email,
            From:          "info@roelofjanelsinga.com",
            MessageStream: "newsletter",
            TemplateModel: map[string]interface{}{
                "title": latestPost.Title,
                "image": latestPost.Image,
                "description": latestPost.Description,
                "url": latestPost.URL,
            },
        }).
        Get(ctx, nil); err != nil {
        return err
    }

    return nil
}

Deze workflow haalt de laatste post op en controleert of het in de afgelopen 7 dagen is geplaatst. Als er in de afgelopen 7 dagen geen nieuwe post is geweest, is de workflow klaar en plant het een nieuwe workflow voor volgende week in.

Als er in de afgelopen 7 dagen een nieuwe post was, voert de workflow een activity uit die "email.send" heet, met de gegevens die de activity nodig heeft (Config).

Deze activity roept de Postmark API aan en stuurt het template met alias "weekly-newsletter" naar het contact dat we aan de workflow gegeven hebben. In deze post hebben we deze e-mail hard gecodeerd als "hello@example.com". De TemplateModel map in de configuratie zijn de variabelen die je in je Postmark template hebt gedefinieerd. Je kunt ook kiezen om een struct te gebruiken in plaats van een map, maar ik hergebruik deze "email.send" activity voor elke workflow die emails verstuurt, dus dit maakt hem gemakkelijker te gebruiken voor mijn toepassing.

De activiteit voor het verzenden van e-mail is vrij eenvoudig en zet gewoon de Config om in een API oproep aan Postmark:

type PostmarkResponse struct {
    To          string
    SubmittedAt time.Time
    MessageID   string
    ErrorCode   int
    Message     string
}

type PostmarkFailure struct {
    ErrorCode int
    Message   string
}

func SendPostmarkTemplate(_ context.Context, config Config) (*PostmarkResponse, error) {

    client := sling.New().Base("https://api.postmarkapp.com")

    var success PostmarkResponse
    var failure PostmarkFailure

    _, err := client.
        Post("/email/withTemplate").
        Add("Accept", "application/json").
        Add("X-Postmark-Server-Token", "your-token-here").
        BodyJSON(Body{
            From:          config.From,
            To:            config.Email,
            TemplateID:    config.TemplateID,
            TemplateAlias: config.TemplateAlias,
            TemplateModel: config.TemplateModel,
            MessageStream: config.MessageStream,
        }).
        Receive(&success, &failure)

    if err != nil {
        return &PostmarkResponse{
            ErrorCode: failure.ErrorCode,
            Message:   failure.Message,
        }, err
    }

    return &success, nil
}

We kunnen onze contacten nu abonneren op onze mailinglijst en hen wekelijks een e-mail sturen! Helaas blijft niet elk contact voor onbepaalde tijd ingeschreven, dus moeten we afmeldingen afhandelen. Laten we eens kijken hoe we dat kunnen doen!

Afmeldingen van de nieuwsbrief afhandelen

Één van je contacten wil zich afmelden van je mailinglijst, dat is jammer! Het is echter niet moeilijk om dit in onze applicatie te implementeren, want we hebben hier al aan gedacht toen we aan onze eerste workflow begonnen!

Herinner je je het workflow ID? Die erg mooie en gemakkelijk te gebruiken workflow ID? Dat zal dit uitschrijfproces veel...veel gemakkelijker maken!

Toen we de workflow voor dit contact uitvoerden, hebben we het workflow ID aangepast om het emailadres van het contact te bevatten: nieuwsbrief-hello@example.com. Nu hebben we alleen nog maar hun email nodig om een contact uit te schrijven uit onze nieuwsbrief.

Omdat we een workflow met een cron schema gebruiken, kunnen we de workflow niet zomaar annuleren, want daardoor wordt een nieuwe workflow ingepland. We willen dat de workflows niet meer gepland worden nadat het contact zich afmeldt, dus moeten we de workflow beëindigen.

Zo ziet het er uit:

type UnsubscribeRequest struct {
    Email string `json:"email"`
}

func (s service) Unsubscribe(ctx context.Context, payload UnsubscribeRequest) error {
    err := s.client.TerminateWorkflow(ctx, fmt.Sprintf(WorkflowID, payload.Email), "", "afmelding")

    if err != nil {
        s.logger.Error("Error terminating workflow", err)
        return err
    }

    return nil
}

De lege string die we aan de TerminateWorkflow methode doorgeven stelt het RunID voor. Door dit RunID leeg te laten, neemt Temporal aan dat je de laatste run voor deze workflow wilt beëindigen.

In de code zie je ook "afmelding" staan, dit is de reden van opzegging. Het is een optioneel iets om toe te voegen, maar het is prettig als je in een team werkt en je afvraagt waarom je workflow wordt beëindigd.

Conclusie

In deze post heb ik beschreven hoe ik mijn nieuwsbrieven van Mailchimp naar Postmark + Temporal heb gemigreerd. Nu, met Postmark en Temporal, heb ik volledige controle over wie, hoe, en wat ik naar mijn mailinglijst stuur. Het voordeel is dat ik nog steeds alle dingen kan doen die ik met Mailchimp kon, voor een fractie van de kosten. Het nadeel van dit alles zelf bouwen is dat je technische expertise moet hebben en alle problemen die zich voordoen zelf moet oplossen.

Met die minpunten kan ik leven, want werken met zowel Postmark als Temporal is een genot! Als iets onduidelijk is, heeft Temporal een geweldige gemeenschap om je te helpen.

Als je vragen hebt over dit proces, aarzel dan niet om contact op te nemen!

Gepubliceerd op: March 4th, 2022

Ik help je met het behalen van geweldige SEO, hogere conversies, en grotere groei van jouw bedrijf

Neem nu contact op om jouw bedrijf te groeien

Roelof Jan Elsinga