DEV Community

Cover image for Go Smtp ve RabbitMQ ile Email servisi
Tayfur for Golang

Posted on

10 4 1 1 1

Go Smtp ve RabbitMQ ile Email servisi

Selamlar Ben Tayfur Kaya yeni Golang öğreniyorum ve size öğrenirken yaşadığım zorlukları ve avantajları bu projede göstermek istiyorum.
Bugün go dili ile yazmış olduğum basit bir email service inceleyeceğiz.
kullandığım teknolojiler RabbitMQ, Smtp, fiber ve Gorm
Github Reposuna gitmek istiyorsanız Github

Stmp

Golang ile basit bir sekilde gönderici olan mail adresine giriş yaparak istediğimiz email adresine mesajı html template olarak göndereceğiz

Html nasıl parse edilir ?

package parseHtml
import (
"bytes"
"fmt"
"html/template"
"log"
)
// htmli parse ediyoruz bazi htmller parametre alabilir bunun ornegi icin weeklyreporttemplate bakmanizi oneririm
func FeatureNotificationTemplate() string {
var templateBuffer bytes.Buffer
t,err := template.ParseFiles("./Templates/FeatureNotification.html")
mimeHeaders := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
//subject gonderilen mailin titleni iceriyor
templateBuffer.Write([]byte(fmt.Sprintf("Subject: Lates Features \n%s\n\n", mimeHeaders)))
t.Execute(&templateBuffer, nil)
if err != nil {
log.Fatal(err)
return ""
}
return templateBuffer.String()
}

 template.ParseFiles("./Templates/FeatureNotification.html")
Enter fullscreen mode Exit fullscreen mode

yukarıda html dosyasını parse ediyoruz.
ve aşağıdaki gibi execute ediyoruz buradaki subject kısmı ise gönderilecek mailin title yazıyoruz..

templateBuffer.Write([]byte(fmt.Sprintf("Subject: Lates Features \n%s\n\n", mimeHeaders)))
    t.Execute(&templateBuffer, nil)
Enter fullscreen mode Exit fullscreen mode

Smtp ile mail göndermek

package mail
import (
"SendEmail-Service/pkg/config"
"SendEmail-Service/pkg/parseHtml"
"fmt"
"net/smtp"
)
// maili gonderecek olan maile giris yapiyoruz
func auth(senderMail string, senderPassword string, smtpHost string) smtp.Auth {
auth := smtp.PlainAuth("", senderMail, senderPassword, smtpHost)
return auth
}
func SendMail(to []string, tamplate string) error {
senderMail := config.Config("SENDER_MAIL")
password := config.Config("SENDER_PASSWORD")
smtpHost := config.Config("SMTP_HOST")
smtpPort := config.Config("SMTP_PORT")
var body string
auth := auth(senderMail, password, smtpHost)
//gonderilmek istenilen template gore parse ediyoruz
switch tamplate {
case "WelcomeQueue":
body = parseHtml.WelcomeTemplate()
break
case "WeeklyReportQueue":
body = parseHtml.WeeklyReportTemplate()
break
case "FeatureNotificationQueue":
body = parseHtml.FeatureNotificationTemplate()
break
}
// parse edilmis html templati ile maili gonderiyoruz
err := smtp.SendMail(smtpHost+":"+smtpPort, auth, senderMail, to, []byte(body))
if err != nil {
fmt.Println(err)
return err
}
fmt.Println("Email Sent!")
return nil
}
view raw sendMail.go hosted with ❤ by GitHub

sendMail fonksiyonu hangi consumerden triggerlandıysa ilgili htmli parse ediyoruz

    switch tamplate {
    case "WelcomeQueue":
        body = parseHtml.WelcomeTemplate()
        break
    case "WeeklyReportQueue":
        body = parseHtml.WeeklyReportTemplate()
        break
    case "FeatureNotificationQueue":
        body = parseHtml.FeatureNotificationTemplate()
        break
    }
Enter fullscreen mode Exit fullscreen mode
err := smtp.SendMail(smtpHost+":"+smtpPort, auth, senderMail, to, []byte(body))
Enter fullscreen mode Exit fullscreen mode

yukaradaki kod bloğunda mail adresimize authentication oluyoruz ve oldugumuz email adresinden to göndereceğimiz mail adresini içeriyor. msg kısmı ise gönderilecek olan mesajı içeriyor ama biz orada bir mesaj yerine html template göndereceğiz.

RabbitMQ

diyebilirsinizki yukarıda basit yazıyordu ne gerek var rabbitMQ kullanmaya!
Image description
Evet var çünkü gönderecek oldugumuz mailleri Message queue
ya göndermemiz gerekiyor, yaptıgımız app belki databaseden aldığımız mail adreslerine email atarken yarısında app crashe olabilir ve biz hangi mail adreslerine mail gönderildi hangisine gönderilmedi bunu bilemeyiz.

Birden fazla email templatemiz olabilir mesela welcome, verify, Feature Notification gibi farkettiginiz gibi bazıları Bulk(toplu gönderilen) mail bazıları Transaction(tekli ) mail.

Biz her template icin bir Consume ve Publish oluşturacağız böylelikle mail adresleri ilk önce queue gidicek sonra Consume"a iletilecek ve oradan templatine göre smtp ile maili göndereceğiz =)

Publish

RabbitMQ bağlantısı oluşturuyoruz

connectRabbitMQ, err := amqp.Dial(amqpServerURL)

Enter fullscreen mode Exit fullscreen mode

consume ile haberleşmek için bir channel oluşturuyoruz

channelRabbitMQ, err := connectRabbitMQ.Channel()


Enter fullscreen mode Exit fullscreen mode

yeni bir mesaj oluşturup bunu queue gönderiyoruz ama burdaki en önemli nokta verilen queue name"i çok önemli bu fonksiyonun çağrıldığı yerden alıyor ve mesajı o queue ismine göre gönderiyor bu sayede consume ederken her template icin farklı queue den onları alabileceğiz

    message := amqp.Publishing{
        ContentType: "text/plain",
        Body:        msg,
    }

    // mesaji publish eder.
    if err := channelRabbitMQ.Publish(
        "",// exchange
        queueName,// queue name
        false,
        false,
        message,
    ); err != nil {
        return err
    }

Enter fullscreen mode Exit fullscreen mode
package rabbitmq
import (
"SendEmail-Service/pkg/config"
"github.com/streadway/amqp"
)
func Publisher(msg []byte, queueName string) error {
//rabbitmq url
amqpServerURL := config.Config("RABBITMQ_URL")
// yeni bir rabbitmq bağlantısı oluştur
connectRabbitMQ, err := amqp.Dial(amqpServerURL)
if err != nil {
panic(err)
}
defer connectRabbitMQ.Close()
// yeni bir kanal oluştur
channelRabbitMQ, err := connectRabbitMQ.Channel()
if err != nil {
panic(err)
}
defer channelRabbitMQ.Close()
_, err = channelRabbitMQ.QueueDeclare(
queueName, // queue name
true,
false,
false,
false,
nil,
)
if err != nil {
panic(err)
}
//yeni mesaj oluştur
message := amqp.Publishing{
ContentType: "text/plain",
Body: msg,
}
// mesaji publish eder.
if err := channelRabbitMQ.Publish(
"",// exchange
queueName,// queue name
false,
false,
message,
); err != nil {
return err
}
return nil
}
view raw publisher.go hosted with ❤ by GitHub

Consume

buradaki işlemlerde aynı sayılır ama bir farkla =)
queue deki mesajları okuyup her mesaj için bir sendMail fonksiyonunu trigger ediyor

    forever := make(chan bool)
    go func() {
        for message := range messages {
            to := []string{string(message.Body)}
            //queue'dan gelen mesaji mail.Send fonksiyonuna gönder
            mail.SendMail(to, queueName)
            log.Printf(" > Received message: %s\n", message.Body)
        }
    }()

    <-forever
Enter fullscreen mode Exit fullscreen mode
package rabbitmq
import (
"SendEmail-Service/mail"
"SendEmail-Service/pkg/config"
"log"
"github.com/streadway/amqp"
)
func Consume(queueName string) {
//rabbitmq url
amqpServerURL := config.Config("RABBITMQ_URL")
// yeni bir rabbitmq bağlantısı oluştur
connectRabbitMQ, err := amqp.Dial(amqpServerURL)
if err != nil {
panic(err)
}
defer connectRabbitMQ.Close()
// yeni bir kanal oluştur
channelRabbitMQ, err := connectRabbitMQ.Channel()
if err != nil {
panic(err)
}
defer channelRabbitMQ.Close()
// queue deki mesajları dinle
messages, err := channelRabbitMQ.Consume(
queueName, // queue name
"",
true,
false,
false,
false,
nil,
)
if err != nil {
log.Println(err)
}
log.Println("Successfully connected to " + queueName + " queue.")
//channelden gelen mesajlari loopa döndür
forever := make(chan bool)
go func() {
for message := range messages {
to := []string{string(message.Body)}
//queue'dan gelen mesaji mail.Send fonksiyonuna gönder
mail.SendMail(to, queueName)
log.Printf(" > Received message: %s\n", message.Body)
}
}()
<-forever
}
view raw consume.go hosted with ❤ by GitHub

Bulk Mail & Transaction Mail

Yukarıda bu ikisinden bahsetmiştik genel anlamda toplu gönderilen maillere Bulk ,ve sadece tekli işlemler için gönderilen maillere Transaction mail denir

    db, err := db.ConnectDB()
    if err != nil {
        log.Fatal(err)
        panic(err)
    }
    var User []models.User
    db.Find(&User)

Enter fullscreen mode Exit fullscreen mode

yukarıda diye bağlanıp dbdeki User ların hepsini arrayin içine alıyoruz
Aşağıda is User arrayini for döngüsüne alıp her user"ın maili için []byte a dönüştürüp bunu queue gönderiyoruz burada []byte a dönüştürmemizin sebebi RabbitMQ channeli byte ile haberleşmesi

    for _, t := range User {
        var to []byte
        to = []byte(t.Email)
        rabbitmq.Publisher(to, template)
    }

Enter fullscreen mode Exit fullscreen mode
package logic
import (
"SendEmail-Service/pkg/db"
"SendEmail-Service/pkg/models"
"SendEmail-Service/pkg/rabbitmq"
"log"
)
func BulkMail(template string) error {
// database baglaniyoruz
db, err := db.ConnectDB()
if err != nil {
log.Fatal(err)
panic(err)
}
var User []models.User
db.Find(&User)
for _, t := range User {
// to parametresini queue'a gondermek icin byte dizisi olarak aliyoruz
var to []byte
to = []byte(t.Email)
rabbitmq.Publisher(to, template)
// db deki userların email adreslerini alıp rabbitmq'e gönderiyoruz , her mail icin bir mesaj gönderiyoruz!!
}
return nil
}
view raw bulkMail.go hosted with ❤ by GitHub

Transaction maillerinde ise farklı farklı işlemler yapılabilir ama biz basit bir halde fiber endpointinden gelen mail adresine email göndereceğiz

package logic
import (
"SendEmail-Service/pkg/rabbitmq"
"log"
)
func Transactional(email []byte,template string)error{
if email==nil{
log.Println("email is empty")
return nil
}
err:=rabbitmq.Publisher(email,template)
return err
}

Main

Main fonksiyonu içinde goroutine ile bütün consumeleri dinliyoruz bunlar hep açık kalıyor queue herhangi bir mail girdiğinde direkt bunları işleme gönderecek

WelcomeQueue := "WelcomeQueue"
    WeeklyReportQueue := "WeeklyReportQueue"
    FeatureNotificationQueue := "FeatureNotificationQueue"
    // consumelari dinliyoruz
    go rabbitmq.Consume(WeeklyReportQueue)
    go rabbitmq.Consume(FeatureNotificationQueue)
    go rabbitmq.Consume(WelcomeQueue)

Enter fullscreen mode Exit fullscreen mode

Dediğim gibi Mailleri gönderirken fiber endpointleri trigger edicek aslında daha complex hale getirilebilir örneğin eğer diye yeni bir kullanıcı eklendiyse o indexteki maile welcomeTemplati göndermek gibi ama şimdi daha basit bir şekilde handle ediyoruz

    app := fiber.New()
    app.Use(
        logger.New(), // add simple logger
    )

    app.Get("/FeatureNotification", func(c *fiber.Ctx) error {
        go logic.BulkMail(FeatureNotificationQueue)
        return c.SendString("Feature notification Sended")
    })
    app.Get("/WelcomeNotification", func(c *fiber.Ctx) error {
        msg := []byte(c.Query("msg"))
        logic.Transactional(msg,WelcomeQueue)
        return c.SendString("Welcome Sended to "+string(msg))
    })
Enter fullscreen mode Exit fullscreen mode
package main
import (
"SendEmail-Service/logic"
"SendEmail-Service/pkg/rabbitmq"
"log"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
)
func main() {
WelcomeQueue := "WelcomeQueue"
WeeklyReportQueue := "WeeklyReportQueue"
FeatureNotificationQueue := "FeatureNotificationQueue"
// consumelari dinliyoruz
go rabbitmq.Consume(WeeklyReportQueue)
go rabbitmq.Consume(FeatureNotificationQueue)
go rabbitmq.Consume(WelcomeQueue)
// Create a new Fiber instance.
app := fiber.New()
app.Use(
logger.New(), // add simple logger
)
//bulk maili trigger etmek icin bu endpoint i kullanıyoruz
app.Get("/FeatureNotification", func(c *fiber.Ctx) error {
go logic.BulkMail(FeatureNotificationQueue)
return c.SendString("Feature notification Sended")
})
//transactional maili trigger etmek icin bu endpoint i kullanıyoruz
app.Get("/WelcomeNotification", func(c *fiber.Ctx) error {
msg := []byte(c.Query("msg"))
logic.Transactional(msg,WelcomeQueue)
return c.SendString("Welcome Sended to "+string(msg))
})
log.Fatal(app.Listen(":3000"))
}
view raw main.go hosted with ❤ by GitHub

conclusion

Bence go yazması çok keyifli bir dil ve kolayca service oluşturabiliyorsunuz eğer sizde benim gibi başka yazılım dillerinden geldiyseniz variables'a önem verin çünkü verileri çok defa convert etmem gerekti
umarım yazımı beğenmişsinizdir, yazım yanlışları için özür dilerim elimden geldiğince anlaşılır yazmaya çalıştım

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

Top comments (1)

Collapse
 
homavand profile image
mohammad babaha

hariiiikaaa

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more