Email Notification System

Overview

The project has a comprehensive email notification system implemented using RabbitMQ for asynchronous email delivery. The backend service publishes email messages to RabbitMQ queues, and an external email consumer service handles the actual email delivery.

Architecture

The system uses RabbitMQ for asynchronous email delivery with the following components:

┌─────────────────────────────────────────────────────────────────┐
│                    Email Notification Flow                      │
│                                                                 │
│  Business Logic (Service Layer)                                 │
│              ↓                                                  │
│  Create PublishEmailMessage                                     │
│              ↓                                                  │
│  Call rabbitMQEmailService.PublishEmailMessage()                │
│              ↓                                                  │
│  Marshal to JSON                                                │
│              ↓                                                  │
│  Publish to RabbitMQ (exchange + queue)                         │
│              ↓                                                  │
│  External Email Consumer Service                                │
│              ↓                                                  │
│  Send actual email                                              │
└─────────────────────────────────────────────────────────────────┘

Components

1. Email Service (app/service/rabbit_mq/mailing/)

Service Layer:

Message Structure:

type PublishEmailMessage struct {
    SendTo  string                 // Recipient email
    Type    string                 // Email template type
    Name    string                 // Recipient name
    Subject string                 // Email subject
    Data    map[string]interface{} // Template data
}

Implementation:

func (r *ServiceImpl) PublishEmailMessage(message *PublishEmailMessage) {
    r.rabbitMQService.PublishMessage(
        message,
        os.Getenv("RABBITMQ_EMAIL_EXCHANGE"),
        os.Getenv("RABBITMQ_EMAIL_QUEUE")
    )
}

2. Email Types Supported

The system supports 11 different email types (defined in app/service/rabbit_mq/mailing/service.go):

Email Type Constant Purpose
Company Member Invitation DI_COMPANY_MEMBER_INVITATION Invite users to join a company
Program Member Invitation DI_PROGRAM_MEMBER_INVITATION Invite users to join a program
Program Member Request DI_PROGRAM_MEMBER_REQUEST Request confirmation to join program
Program Member Added DI_PROGRAM_MEMBER_ADD Notify user they were added to program
Program Creation DI_PROGRAM_CREATION Notify admins of new program creation
ISV Admin Request Approval DI_ISV_REQUEST_ADMIN_APPROVAL Approve ISV admin request
ISV Admin Request Denial DI_ISV_REQUEST_ADMIN_DENY Deny ISV admin request
Verified Professional DI_BECOME_A_VERIFIED_PROFESSIONAL_EMAIL Invite to become verified professional
Contact Support CONTACT_SUPPORT_EMAIL Contact support form submission
Work Email Update Code WORK_EMAIL_UPDATE_CODE Verification code for work email update

3. RabbitMQ Publisher (app/service/rabbit_mq/publisher.go)

Singleton Pattern Implementation:

Key Features:

type Publisher struct {
    connection *amqp.Connection
    channel    *amqp.Channel
    ctx        context.Context
    mutex      sync.Mutex
    ready      chan struct{}
}

Publisher Methods:

Auto-Reconnection Logic: The publisher automatically reconnects to RabbitMQ if the connection is lost:

func (p *Publisher) run() {
    for {
        if err := p.connect(); err != nil {
            log.Errorf("Publisher connection error: %v", err)
            time.Sleep(2 * time.Second)
            continue
        }

        // Wait for connection close notification
        closeErr := <-p.connection.NotifyClose(make(chan *amqp.Error))
        log.Warnf("Publisher connection closed: %v", closeErr)

        // Reconnect after 2 seconds
        time.Sleep(2 * time.Second)
    }
}

Usage Examples

Company Invitation Email

Location: app/service/company/service.go:770

rabbitMessage := new(rabbitMQEmailService.PublishEmailMessage)
rabbitMessage.SendTo = invitation.Email
rabbitMessage.Name = invitation.Name
rabbitMessage.Type = rabbitMQEmailService.CompanySendInvitationEmail
rabbitMessage.Subject = rabbitMQEmailService.SendInvitationSubject
rabbitMessage.Data = map[string]interface{}{
    "invitation_token": invitation.Token,
    "company_name":     company.Name,
}

s.rabbitMQEmailService.PublishEmailMessage(rabbitMessage)

Program Creation Notification

Location: app/service/program/service.go:94

rabbitEmailMessage := rabbitMQEmailService.PublishEmailMessage{
    Type:    rabbitMQEmailService.ProgramCreation,
    Subject: "New program created: " + programData.Name,
    Data: map[string]interface{}{
        "program_id":   programData.ID.String(),
        "program_name": programData.Name,
        "company_name": programData.CompanyName,
        "created_by":   user.Firstname + " " + user.Lastname,
        "created_at":   time.Now().Format("02 Jan 2006"),
    },
}

go s.rabbitMQEmailService.PublishEmailMessage(&rabbitEmailMessage)

Program Member Invitation

Location: app/service/program/service.go:586

rabbitMessage := new(rabbitMQEmailService.PublishEmailMessage)
rabbitMessage.SendTo = invitation.Email
rabbitMessage.Name = invitation.Name
rabbitMessage.Type = rabbitMQEmailService.ProgramSendInvitationEmail
rabbitMessage.Subject = rabbitMQEmailService.SendInvitationSubject
rabbitMessage.Data = map[string]interface{}{
    "invitation_token": invitation.Token,
    "program_name":     programPage.Name,
    "role":             strings.ToLower(invitation.Role),
}

s.rabbitMQEmailService.PublishEmailMessage(rabbitMessage)

Program Member Added Notification

Location: app/service/program/service.go:519

rabbitMsg := new(rabbitMQEmailService.PublishEmailMessage)
rabbitMsg.SendTo = ownerUser.Email
rabbitMsg.Name = ownerUser.Firstname + " " + ownerUser.Lastname
rabbitMsg.Type = rabbitMQEmailService.ProgramMemberAdd
rabbitMsg.Subject = fmt.Sprintf("You have been added to the %s program", programPage.Name)
rabbitMsg.Data = map[string]interface{}{
    "user_name":    ownerUser.Firstname + " " + ownerUser.Lastname,
    "program_id":   programPage.ID,
    "program_name": programPage.Name,
    "company_name": company.LegalName,
    "admin_name":   user.Firstname + " " + user.Lastname,
    "role":         invitation.Role,
}

go s.rabbitMQEmailService.PublishEmailMessage(rabbitMsg)

Verified Professional Email

Location: app/service/user/service.go:205

rabbitMessage := new(rabbitMQEmailService.PublishEmailMessage)
rabbitMessage.SendTo = email
rabbitMessage.Name = user.Firstname
rabbitMessage.Type = rabbitMQEmailService.ISVBecomeProEmail
rabbitMessage.Subject = rabbitMQEmailService.ISVBecomeProSubject
rabbitMessage.Data = map[string]interface{}{
    "token": token,
}

go s.rabbitMQEmailService.PublishEmailMessage(rabbitMessage)

Work Email Update Code

Location: app/service/user/service.go:337

rabbitEmailMessage := rabbitMQEmailService.PublishEmailMessage{
    SendTo:  email,
    Type:    rabbitMQEmailService.WorkEmailUpdateCode,
    Subject: "Work Email Update",
    Data: map[string]interface{}{
        "code":  code,
        "email": email,
    },
}

s.rabbitMQEmailService.PublishEmailMessage(&rabbitEmailMessage)

Contact Support Email

Location: app/service/billing_center/service.go:25

rabbitMessage := new(rabbitMQEmailService.PublishEmailMessage)
rabbitMessage.SendTo = os.Getenv("CONTACT_SUPPORT_ADMIN_EMAIL")
rabbitMessage.Name = "Admins"
rabbitMessage.Type = rabbitMQEmailService.SendContactSupportEmail
rabbitMessage.Subject = rabbitMQEmailService.SendContactSupportSubject
rabbitMessage.Data = map[string]interface{}{
    "first_name":   input.Firstname,
    "last_name":    input.Lastname,
    "username":     input.Firstname + " " + input.Lastname,
    "email":        input.Email,
    "topic":        input.Topic,
    "description":  input.Description,
    "attachment":   input.Attachment,
}

go s.rabbitMQEmailService.PublishEmailMessage(rabbitMessage)

ISV Admin Request Approval/Denial

Location: app/service/company/service.go:920-958

rabbitMessage := new(rabbitMQEmailService.PublishEmailMessage)
rabbitMessage.SendTo = requestUser.Email
rabbitMessage.Name = requestUser.Firstname + " " + requestUser.Lastname
rabbitMessage.Data = map[string]interface{}{
    "request_user_name": requestUser.Firstname,
    "admin_name":        user.Firstname,
}

switch action {
case constant.Deny:
    invitation.Rejected = true
    rabbitMessage.Type = rabbitMQEmailService.ISVRequestDeny
    rabbitMessage.Subject = rabbitMQEmailService.ISVRequestDenySubject

case constant.Approve:
    invitation.Approved = true
    rabbitMessage.Type = rabbitMQEmailService.ISVRequestApproval
    rabbitMessage.Subject = rabbitMQEmailService.ISVRequestApprovalSubject
}

s.rabbitMQEmailService.PublishEmailMessage(rabbitMessage)

Email Action Routes

The system has email action handlers for link clicks in emails:

Route Configuration: app/route/email_action/email_action.go

emailActions := api.Group("/email-actions", init.AuthMiddleware.ValidateTokenMiddleware())
emailActions.Route("/program", email_action.Program(init))
emailActions.Route("/user", email_action.User(init))

Available Email Actions:

Route Controller Method Purpose
GET /api/email-actions/program/member-join-confirm ProgramController.MemberJoinConfirm Confirm program membership from email link
GET /api/email-actions/user/user-activation UserController.EmailActionActivateUser Activate user account from email link

Environment Configuration

Email-related environment variables (.env.dist):

# RabbitMQ Email Configuration
RABBITMQ_EMAIL_EXCHANGE=mail.exchange.direct
RABBITMQ_EMAIL_QUEUE=mail_service_queue

# Contact Support Admin Emails (comma-separated)
CONTACT_SUPPORT_ADMIN_EMAIL=devinsider-production@wylog.com,diamondrastier@gmail.com,nralambomanana@wylog.com,asimbola@wylog.com

Key Features

Asynchronous Processing - All emails sent via goroutines (go s.rabbitMQEmailService.PublishEmailMessage()) to avoid blocking the main request ✅ Template-based - Uses email type constants for template selection on consumer side ✅ Resilient - Auto-reconnection to RabbitMQ on connection failure ✅ Separation of Concerns - Backend only publishes messages, external email service handles delivery ✅ Multi-recipient Support - Contact support sends to multiple admin emails ✅ Thread-safe - Publisher uses mutex for concurrent access ✅ Type-safe - Email types defined as constants to prevent typos ✅ Flexible Data - Generic map[string]interface{} for template data

Message Flow Diagram

┌────────────────────────────────────────────────────────────────────┐
│                        Email Flow Diagram                          │
└────────────────────────────────────────────────────────────────────┘

┌─────────────┐
│   Service   │  1. Create email message
│   Layer     │     with type, recipient, subject, data
└──────┬──────┘
       │
       ▼
┌──────────────────────┐
│ RabbitMQ Email       │  2. Publish to RabbitMQ
│ Service              │
│ PublishEmailMessage()│
└──────┬───────────────┘
       │
       ▼
┌──────────────────────┐
│ RabbitMQ Service     │  3. Marshal to JSON
│ PublishMessage()     │
└──────┬───────────────┘
       │
       ▼
┌──────────────────────┐
│ RabbitMQ Publisher   │  4. Get singleton instance
│ GetPublisher()       │     Wait for ready signal
└──────┬───────────────┘
       │
       ▼
┌──────────────────────────────────────┐
│            RabbitMQ Server5. Publish to exchangeExchange: mail.exchange.direct      │     Route to queue
│  Queue: mail_service_queue           │
└──────┬───────────────────────────────┘
       │
       ▼
┌──────────────────────┐
│  Email Consumer      │  6. Consume from queue
│  (External Service)  │     Select template by type
│                      │     Render with data
│                      │     Send email via SMTP
└──────────────────────┘

Error Handling

Publisher Errors

Service Errors

Best Practices

When Adding New Email Types

  1. Define constant in app/service/rabbit_mq/mailing/service.go:

    const (
        NewEmailType = "NEW_EMAIL_TYPE"
        NewEmailSubject = "Email Subject"
    )
    
  2. Create email message in service layer:

    rabbitMessage := new(rabbitMQEmailService.PublishEmailMessage)
    rabbitMessage.SendTo = recipient.Email
    rabbitMessage.Name = recipient.Name
    rabbitMessage.Type = rabbitMQEmailService.NewEmailType
    rabbitMessage.Subject = rabbitMQEmailService.NewEmailSubject
    rabbitMessage.Data = map[string]interface{}{
        "key1": "value1",
        "key2": "value2",
    }
    
  3. Publish asynchronously:

    go s.rabbitMQEmailService.PublishEmailMessage(rabbitMessage)
    
  4. Update external email consumer to handle the new email type with corresponding template

Asynchronous vs Synchronous

Use goroutine (async) when:

Use synchronous when:

Most emails in the codebase use goroutines (go s.rabbitMQEmailService.PublishEmailMessage()) for better performance.

Monitoring & Debugging

Logs

The publisher logs important events:

Troubleshooting

Email not sent:

  1. Check RabbitMQ connection is active
  2. Verify queue name and exchange in environment variables
  3. Check external email consumer is running
  4. Review RabbitMQ management UI for message count

Connection failures:

Production Readiness

The email notification system is production-ready with: