BulkSend is a versatile application designed to send bulk emails efficiently using Azure Functions and Redis as a message broker. The application provides two entry points for initiating the email sending process: a console application and an Azure Function. This dual-entry-point approach offers flexibility in how users can trigger bulk email operations, catering to both automated and manual workflows.
This document provides a comprehensive overview of the BulkSend application, including its architecture, components, workflows, setup instructions, and guidelines for future extensions such as SMS and notifications.
BulkSend leverages a serverless architecture facilitated by Azure Functions, ensuring scalability and cost-effectiveness. Redis serves as the message broker, managing the queue of email tasks. The application interacts with SendGrid for email delivery, ensuring reliable and efficient dispatch of bulk emails.
Figure 1: High-Level Architecture of BulkSend Application
@startuml
package "bulksend" {
class RedisProducer {
- ConnectionMultiplexer _redis
- string _queueName
+ RedisProducer(string redisConnectionString, string queueName)
+ void PublishEmailBatch(IEnumerable<string> emails, string subject, string content)
}
class RedisConsumer {
- ConnectionMultiplexer _redis
- string _queueName
- string _sendGridApiKey
+ RedisConsumer(string redisConnectionString, string queueName, string sendGridApiKey)
+ Task StartConsuming(string senderEmail, string senderName, string subject, string content)
- Task ProcessTaskAsync(string serializedTask, string senderEmail, string senderName, string subject, string content)
- Task SendEmails(string[] emails, string senderEmail, string senderName, string subject, string content)
}
class ConfigHelper {
+ static T LoadFromJsonFile<T>(string fileName)
}
class Sender {
+ string SenderEmail
+ string SenderName
}
class Template {
+ string Subject
+ string ContentTemplate
}
class Recipient {
+ string Email
+ string Salutation
}
class AppConfig {
+ RedisConfig Redis
+ SendGridConfig SendGrid
}
class RedisConfig {
+ string ConnectionString
+ string QueueName
}
class SendGridConfig {
+ string ApiKey
}
}
RedisConsumer --> SendGridConfig
RedisProducer --> RedisConfig
AppConfig --> RedisConfig
AppConfig --> SendGridConfig
@enduml
Figure 2: Class Diagram of BulkSend Application
Handles HTTP POST requests to enqueue bulk email tasks into Redis and initiates the consumption of these tasks for processing.
config.json
, sender.json
, template.json
, and recipients.json
to retrieve necessary configurations and data.RedisProducer
.RedisConsumer
to process and send emails via SendGrid.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace bulksend
{
public static class RedisEmailConsumerFunction
{
[FunctionName("RedisEmailConsumerHttpTrigger")]
public static async Task<IActionResult> RunHttpTrigger(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
ILogger log)
{
log.LogInformation($"[{DateTime.Now}] HTTP Trigger: RedisEmailConsumer invoked.");
Console.WriteLine($"[{DateTime.Now}] HTTP Trigger: RedisEmailConsumer invoked.");
AppConfig config;
Sender sender;
Template template;
List<Recipient> recipients;
try
{
log.LogInformation($"[{DateTime.Now}] Loading configuration and JSON data files...");
Console.WriteLine($"[{DateTime.Now}] Loading configuration and JSON data files...");
// Load configuration and data from JSON files
config = ConfigHelper.LoadFromJsonFile<AppConfig>("config.json");
sender = ConfigHelper.LoadFromJsonFile<Sender>("sender.json");
template = ConfigHelper.LoadFromJsonFile<Template>("template.json");
recipients = ConfigHelper.LoadFromJsonFile<List<Recipient>>("recipients.json");
log.LogInformation($"[{DateTime.Now}] Successfully loaded configuration and JSON data files.");
Console.WriteLine($"[{DateTime.Now}] Successfully loaded configuration and JSON data files.");
}
catch (Exception ex)
{
log.LogError($"[{DateTime.Now}] Error loading configuration or JSON data files: {ex.Message}");
Console.WriteLine($"[{DateTime.Now}] Error loading configuration or JSON data files: {ex.Message}");
return new BadRequestObjectResult($"Error loading configuration or JSON data files: {ex.Message}");
}
try
{
log.LogInformation($"[{DateTime.Now}] Publishing tasks to Redis for {recipients.Count} recipients...");
Console.WriteLine($"[{DateTime.Now}] Publishing tasks to Redis for {recipients.Count} recipients...");
// Publish tasks to Redis
var producer = new RedisProducer(config.Redis.ConnectionString, config.Redis.QueueName);
Parallel.ForEach(recipients, recipient =>
{
string personalizedContent = template.ContentTemplate.Replace("{{salutation}}", recipient.Salutation);
producer.PublishEmailBatch(
new List<string> { recipient.Email },
template.Subject,
personalizedContent
);
log.LogInformation($"[{DateTime.Now}] Task published to Redis for recipient: {recipient.Email}");
Console.WriteLine($"[{DateTime.Now}] Task published to Redis for recipient: {recipient.Email}");
});
log.LogInformation($"[{DateTime.Now}] Successfully published tasks to Redis.");
Console.WriteLine($"[{DateTime.Now}] Successfully published tasks to Redis.");
// Start consuming tasks from Redis
log.LogInformation($"[{DateTime.Now}] Starting to process tasks from Redis...");
Console.WriteLine($"[{DateTime.Now}] Starting to process tasks from Redis...");
var consumer = new RedisConsumer(config.Redis.ConnectionString, config.Redis.QueueName, config.SendGrid.ApiKey);
await consumer.StartConsuming(
sender.SenderEmail,
sender.SenderName,
template.Subject,
template.ContentTemplate
);
log.LogInformation($"[{DateTime.Now}] Redis email consumer completed successfully.");
Console.WriteLine($"[{DateTime.Now}] Redis email consumer completed successfully.");
return new OkObjectResult("Redis email consumer process completed successfully.");
}
catch (Exception ex)
{
log.LogError($"[{DateTime.Now}] Error processing tasks from Redis: {ex.Message}");
Console.WriteLine($"[{DateTime.Now}] Error processing tasks from Redis: {ex.Message}");
return new StatusCodeResult(500);
}
}
}
}
RedisEmailConsumerFunction
via the console.RedisEmailConsumerHttpTrigger
function.The function reads config.json
, sender.json
, template.json
, and recipients.json
to gather necessary information.
{{salutation}}
placeholder in the template.RedisProducer
publishes these tasks to the Redis queue named emailTasks
.RedisConsumer
listens to the emailTasks
queue.Throughout the process, logs are generated to monitor the status of task enqueuing, processing, and email dispatch.
Figure 3: Data Flow Diagram of BulkSend Application
Description: Illustrates the high-level components of the BulkSend application and their interactions.
@startuml
package "bulksend" {
[RedisEmailConsumerFunction] --> [RedisProducer]
[RedisEmailConsumerFunction] --> [RedisConsumer]
[RedisProducer] --> [Redis]
[RedisConsumer] --> [SendGrid]
}
database "Redis" as Redis
cloud "SendGrid" as SendGrid
@enduml
Figure 4: Component Diagram of BulkSend Application
Description: Details the classes, interfaces, and their relationships within the BulkSend application.
@startuml
package "bulksend" {
class RedisProducer {
- ConnectionMultiplexer _redis
- string _queueName
+ RedisProducer(string redisConnectionString, string queueName)
+ void PublishEmailBatch(IEnumerable<string> emails, string subject, string content)
}
class RedisConsumer {
- ConnectionMultiplexer _redis
- string _queueName
- string _sendGridApiKey
+ RedisConsumer(string redisConnectionString, string queueName, string sendGridApiKey)
+ Task StartConsuming(string senderEmail, string senderName, string subject, string content)
- Task ProcessTaskAsync(string serializedTask, string senderEmail, string senderName, string subject, string content)
- Task SendEmails(string[] emails, string senderEmail, string senderName, string subject, string content)
}
class ConfigHelper {
+ static T LoadFromJsonFile<T>(string fileName)
}
class Sender {
+ string SenderEmail
+ string SenderName
}
class Template {
+ string Subject
+ string ContentTemplate
}
class Recipient {
+ string Email
+ string Salutation
}
class AppConfig {
+ RedisConfig Redis
+ SendGridConfig SendGrid
}
class RedisConfig {
+ string ConnectionString
+ string QueueName
}
class SendGridConfig {
+ string ApiKey
}
}
RedisConsumer --> SendGridConfig
RedisProducer --> RedisConfig
AppConfig --> RedisConfig
AppConfig --> SendGridConfig
@enduml
Figure 5: Class Diagram of BulkSend Application
Description: Depicts the sequence of interactions when processing and sending bulk emails.
@startuml
actor User
participant "RedisEmailConsumerFunction" as Function
participant "RedisProducer" as Producer
participant "Redis" as Redis
participant "RedisConsumer" as Consumer
participant "SendGrid" as SendGrid
User -> Function: POST /api/RedisEmailConsumerHttpTrigger
Function -> Function: Load configurations from JSON files
Function -> Producer: PublishEmailBatch for each recipient
Producer -> Redis: Enqueue email task
Function -> User: 200 OK
Function -> Consumer: StartConsuming
Consumer -> Redis: Dequeue email task
Redis -> Consumer: Return email task
Consumer -> SendGrid: Send email via API
SendGrid --> Consumer: Response
@enduml
Figure 6: Sequence Diagram of BulkSend Application
@startuml
start
:Initiate Bulk Email Task;
if (Entry Point?) then (Console)
:Run Console Application;
else (Azure Function)
:Send HTTP POST Request;
endif
:Load Configurations and Data;
:Enqueue Email Tasks to Redis;
:Redis Consumer Dequeues Task;
:Process and Send Emails via SendGrid;
if (Success?) then (Yes)
:Log Success;
else (No)
:Handle Failure (Retry/DLQ);
endif
stop
@enduml
Figure 7: Flow Chart of Bulk Email Sending Process
bulksend/ ├── Config/ │ ├── config.json │ ├── sender.json │ ├── template.json │ └── recipients.json ├── bin/ ├── obj/ ├── bulksend.csproj ├── RedisEmailConsumerFunction.cs ├── RedisProducer.cs ├── RedisConsumer.cs ├── JsonHelper.cs ├── Sender.cs ├── Template.cs ├── Recipient.cs └── ConfigHelper.cs
Ensure that all configuration files (config.json
, sender.json
, template.json
, recipients.json
) are placed within the Config
directory at the root of the project.
Example Configuration Paths:
bulksend/Config/config.json
bulksend/Config/sender.json
bulksend/Config/template.json
bulksend/Config/recipients.json
The application uses the ConfigHelper
class to load and deserialize JSON configuration files. Ensure that the file paths are correctly referenced relative to the application's base directory.
public static T LoadFromJsonFile<T>(string fileName)
{
string basePath = AppContext.BaseDirectory;
string filePath = Path.Combine(basePath, "Config", fileName);
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"File not found: {filePath}");
}
try
{
string jsonContent = File.ReadAllText(filePath);
return JsonSerializer.Deserialize<T>(jsonContent)
?? throw new InvalidOperationException("Failed to deserialize JSON data.");
}
catch (JsonException ex)
{
throw new InvalidOperationException($"Error parsing JSON file {fileName}: {ex.Message}");
}
}
git clone https://github.com/mjaffry01/bulksend.git
cd bulksend
Verify that the Config
folder contains config.json
, sender.json
, template.json
, and recipients.json
.
dotnet restore
dotnet build
(Assuming a separate console application exists for BulkSend)
cd bulksend.ConsoleApp
dotnet run
Expected Output:
[Timestamp] Console Trigger: RedisEmailConsumer invoked. [Timestamp] Loading configuration and JSON data files... [Timestamp] Successfully loaded configuration and JSON data files. [Timestamp] Publishing tasks to Redis for X recipients... [Timestamp] Task published to Redis for recipient: email@example.com ... [Timestamp] Successfully published tasks to Redis. [Timestamp] Starting to process tasks from Redis... [Timestamp] Email sent to email@example.com ... [Timestamp] Redis email consumer completed successfully.
choco install azure-functions-core-tools-4
brew tap azure/functions
brew install azure-functions-core-tools@4
cd bulksend
func start --verbose
Expected Output:
Host lock lease acquired by instance ID '00000000000000000000000039564EDC'. Executed 'RedisEmailConsumerHttpTrigger' (Failed, Id=64e0f8c4-fb0d-44a9-bab9-863280fed446, Duration=134ms) System.Private.CoreLib: Exception has been thrown by the target of an invocation. SendBulkSend: JSON file not found: Config/config.json.
Note: The above output indicates an error where config.json
was not found. Ensure that the configuration files are correctly placed and the paths are accurate.
Ensure that all JSON configuration files are present in the Config
directory and that the application has the correct file paths to access them.
Verify that the Redis server is running and accessible via the connection string provided in config.json
.
Ensure that the SendGrid API key is valid and has the necessary permissions. Also, verify that the sender email is verified in SendGrid.
To extend the BulkSend application to handle SMS and notifications, follow these steps:
Update config.json
to include Twilio settings.
{
"Redis": {
"ConnectionString": "bulkmessages.redis.cache.windows.net:6380,password=mtRh8pkazc0GOwgYS64r1pJzIe0pQHkrfAzCaKruBn4=,ssl=True",
"QueueName": "emailTasks"
},
"SendGrid": {
"ApiKey": "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"
},
"Twilio": {
"AccountSid": "your_twilio_account_sid",
"AuthToken": "your_twilio_auth_token",
"FromPhoneNumber": "+1234567890"
}
}
Define a class to represent SMS tasks.
public class SMSTask
{
public string[] PhoneNumbers { get; set; }
public string Message { get; set; }
}
Implement functionality to send SMS via Twilio.
using Twilio;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;
using System;
using System.Threading.Tasks;
public class SMSSender
{
private readonly string _accountSid;
private readonly string _authToken;
private readonly string _fromPhoneNumber;
public SMSSender(string accountSid, string authToken, string fromPhoneNumber)
{
_accountSid = accountSid;
_authToken = authToken;
_fromPhoneNumber = fromPhoneNumber;
TwilioClient.Init(_accountSid, _authToken);
}
public async Task SendSMSAsync(string toPhoneNumber, string message)
{
var to = new PhoneNumber(toPhoneNumber);
var from = new PhoneNumber(_fromPhoneNumber);
var msg = await MessageResource.CreateAsync(
to: to,
from: from,
body: message
);
Console.WriteLine($"SMS sent to {toPhoneNumber}: SID {msg.Sid}");
}
}
Modify RedisConsumer
to recognize and process SMS tasks.
public class RedisConsumer
{
// Existing members...
public async Task StartConsuming(string senderEmail, string senderName, string subject, string content)
{
// Existing code...
while (true)
{
var serializedTask = await db.ListLeftPopAsync(_queueName);
if (!serializedTask.IsNullOrEmpty)
{
if (IsEmailTask(serializedTask))
{
await ProcessEmailTaskAsync(serializedTask, senderEmail, senderName, subject, content);
}
else if (IsSMSTask(serializedTask))
{
await ProcessSMSTaskAsync(serializedTask);
}
}
else
{
// No task found, wait before retrying
await Task.Delay(1000);
}
}
}
private bool IsSMSTask(string serializedTask)
{
return serializedTask.Contains("PhoneNumbers");
}
private async Task ProcessSMSTaskAsync(string serializedTask)
{
try
{
var task = JsonSerializer.Deserialize<SMSTask>(serializedTask);
if (task != null && task.PhoneNumbers != null && task.PhoneNumbers.Length > 0)
{
var smsSender = new SMSSender(_twilioAccountSid, _twilioAuthToken, _twilioFromPhoneNumber);
foreach (var phoneNumber in task.PhoneNumbers)
{
await smsSender.SendSMSAsync(phoneNumber, task.Message);
}
}
}
catch (JsonException ex)
{
Console.WriteLine($"Failed to deserialize SMS task: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Error processing SMS task: {ex.Message}");
}
}
// Existing SendEmails method...
}
// Example: Enqueue SMS Tasks
var smsTask = new SMSTask
{
PhoneNumbers = new string[] { "+1234567890", "+0987654321" },
Message = "Hello! This is a test SMS from BulkSend."
};
producer.PublishSMSTask(smsTask);
Modify the HTTP trigger to accept parameters for SMS tasks or define separate triggers as needed.
public class RedisProducer
{
// Existing members...
public void PublishSMSTask(SMSTask smsTask)
{
var db = _redis.GetDatabase();
var serializedTask = JsonSerializer.Serialize(smsTask);
db.ListRightPush(_queueName, serializedTask);
Console.WriteLine($"Published SMS task to Redis: {serializedTask}");
}
}
Objective: Integrate a notification system (e.g., push notifications) to enhance the application's communication capabilities.
public class NotificationTask
{
public string[] DeviceTokens { get; set; }
public string Title { get; set; }
public string Message { get; set; }
}
Utilize a service like Firebase Cloud Messaging (FCM) or similar.
using FirebaseAdmin;
using FirebaseAdmin.Messaging;
using Google.Apis.Auth.OAuth2;
using System;
using System.Threading.Tasks;
public class NotificationSender
{
public NotificationSender(string serviceAccountPath)
{
FirebaseApp.Create(new AppOptions()
{
Credential = GoogleCredential.FromFile(serviceAccountPath),
});
}
public async Task SendNotificationAsync(string[] deviceTokens, string title, string message)
{
var notification = new Notification
{
Title = title,
Body = message
};
var messageToSend = new MulticastMessage()
{
Tokens = deviceTokens,
Notification = notification
};
var response = await FirebaseMessaging.DefaultInstance.SendMulticastAsync(messageToSend);
Console.WriteLine($"Successfully sent {response.SuccessCount} notifications; {response.FailureCount} failures.");
}
}
public class RedisConsumer
{
// Existing members...
public async Task StartConsuming(string senderEmail, string senderName, string subject, string content)
{
// Existing code...
while (true)
{
var serializedTask = await db.ListLeftPopAsync(_queueName);
if (!serializedTask.IsNullOrEmpty)
{
if (IsEmailTask(serializedTask))
{
await ProcessEmailTaskAsync(serializedTask, senderEmail, senderName, subject, content);
}
else if (IsSMSTask(serializedTask))
{
await ProcessSMSTaskAsync(serializedTask);
}
else if (IsNotificationTask(serializedTask))
{
await ProcessNotificationTaskAsync(serializedTask);
}
}
else
{
// No task found, wait before retrying
await Task.Delay(1000);
}
}
}
private bool IsNotificationTask(string serializedTask)
{
return serializedTask.Contains("DeviceTokens");
}
private async Task ProcessNotificationTaskAsync(string serializedTask)
{
try
{
var task = JsonSerializer.Deserialize<NotificationTask>(serializedTask);
if (task != null && task.DeviceTokens != null && task.DeviceTokens.Length > 0)
{
var notificationSender = new NotificationSender("path_to_service_account.json");
await notificationSender.SendNotificationAsync(task.DeviceTokens, task.Title, task.Message);
}
}
catch (JsonException ex)
{
Console.WriteLine($"Failed to deserialize Notification task: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Error processing Notification task: {ex.Message}");
}
}
// Existing methods...
}
public class RedisProducer
{
// Existing members...
public void PublishNotificationTask(NotificationTask notificationTask)
{
var db = _redis.GetDatabase();
var serializedTask = JsonSerializer.Serialize(notificationTask);
db.ListRightPush(_queueName, serializedTask);
Console.WriteLine($"Published Notification task to Redis: {serializedTask}");
}
}
// Example: Enqueue Notification Tasks
var notificationTask = new NotificationTask
{
DeviceTokens = new string[] { "token1", "token2" },
Title = "Welcome",
Message = "Thank you for joining our service!"
};
producer.PublishNotificationTask(notificationTask);
Modify the HTTP trigger to accept notification task details or create separate triggers for notification tasks.
Implement webhooks to notify external systems about the status of email/SMS/notification deliveries.
Enhance resilience by implementing sophisticated retry policies using Polly or similar libraries to handle transient failures.
Introduce DLQs to handle tasks that consistently fail after multiple retry attempts, ensuring that problematic tasks are isolated and can be reviewed or reprocessed manually.
The BulkSend application is a robust solution for sending bulk emails, with scalable architecture facilitated by Azure Functions and Redis. Its modular design allows for easy extensions, such as integrating SMS and notification services, ensuring that the application can evolve to meet diverse communication needs. By adhering to best practices in configuration management, dependency injection, and error handling, BulkSend ensures reliability and maintainability, making it well-suited for production environments and future enhancements.
For further enhancements, consider integrating additional communication channels, implementing advanced monitoring and logging, and optimizing performance based on usage patterns and feedback.
Ready to explore the BulkSend application? Click the button below to download the complete project from GitHub.
Download Project
{
"Redis": {
"ConnectionString": "bulkmessages.redis.cache.windows.net:6380,password=mtRh8pkazc0GOwgYS64r1pJzIe0pQHkrfAzCaKruBn4=,ssl=True",
"QueueName": "emailTasks"
},
"SendGrid": {
"ApiKey": "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"
},
"Twilio": {
"AccountSid": "your_twilio_account_sid",
"AuthToken": "your_twilio_auth_token",
"FromPhoneNumber": "+1234567890"
},
"Firebase": {
"ServiceAccountPath": "path_to_service_account.json"
}
}
{
"SenderEmail": "mjaffry014@gmail.com",
"SenderName": "Md Ali"
}
{
"Subject": "Welcome to Our Service",
"ContentTemplate": "Dear {{salutation}},\n\nWe are thrilled to have you on board!\n\nThank you,\nThe Team"
}
[
{
"Email": "mjaffry02@gmail.com",
"Salutation": "Mr. Ali"
},
{
"Email": "mjaffry014@gmail.com",
"Salutation": "Dr. Jaffrey"
},
{
"Email": "mjaffry04@outlook.com",
"Salutation": "Ms. Fatima"
}
]