Skip to content

Commit

Permalink
Added support to receive and save email open notifications.
Browse files Browse the repository at this point in the history
  • Loading branch information
n.bitounis committed May 11, 2020
1 parent 35a11ec commit 5de543a
Show file tree
Hide file tree
Showing 14 changed files with 300 additions and 19 deletions.
29 changes: 29 additions & 0 deletions Projects/SesNotifications.App/Factories/DbSesOpenFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using SesNotifications.App.Models;
using SesOpen = SesNotifications.DataAccess.Entities.SesOpen;

namespace SesNotifications.App.Factories
{
public static class DbSesOpenFactory
{
public static SesOpen Create(this SesOpenModel open, long notificationId)
{
return new SesOpen
{
Id = notificationId,
NotificationId = notificationId,
NotificationType = "Open",
SentAt = Convert.ToDateTime(open.Mail.Timestamp),
MessageId = open.Mail.MessageId,
Source = open.Mail.Source,
SourceArn = open.Mail.SourceArn,
SourceIp = open.Mail.SourceIp,
SendingAccountId = open.Mail.SendingAccountId,
Recipients = string.Join(',', open.Mail.Destination),
OpenedAt = Convert.ToDateTime(open.Open.Timestamp),
UserAgent = open.Open.UserAgent,
IpAddress = open.Open.IpAddress
};
}
}
}
1 change: 1 addition & 0 deletions Projects/SesNotifications.App/Models/Ses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
public class Ses
{
public virtual string NotificationType { get; set; }
public virtual string EventType { get; set; }
}
}
9 changes: 9 additions & 0 deletions Projects/SesNotifications.App/Models/SesOpen.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace SesNotifications.App.Models
{
public class SesOpen
{
public virtual string Timestamp { get; set; }
public virtual string UserAgent { get; set; }
public virtual string IpAddress { get; set; }
}
}
8 changes: 8 additions & 0 deletions Projects/SesNotifications.App/Models/SesOpenModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace SesNotifications.App.Models
{
public class SesOpenModel : Ses
{
public virtual SesMail Mail { get; set; }
public virtual SesOpen Open { get; set; }
}
}
61 changes: 49 additions & 12 deletions Projects/SesNotifications.App/Services/NotificationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,27 @@ public class NotificationService : INotificationService
private const string Bounce = "bounce";
private const string Delivery = "delivery";
private const string Complaint = "complaint";
private const string Open = "open";

private readonly INotificationsRepository _notificationsRepository;
private readonly ISesBouncesRepository _sesBouncesRepository;
private readonly ISesComplaintsRepository _sesComplaintsRepository;
private readonly ISesDeliveriesRepository _sesDeliveriesRepository;
private readonly ISesOpensRepository _sesOpensRepository;
private readonly ILogger<NotificationService> _logger;

public NotificationService(INotificationsRepository notificationsRepository,
ISesBouncesRepository sesBouncesRepository,
ISesComplaintsRepository sesComplaintsRepository,
ISesDeliveriesRepository sesDeliveriesRepository,
ISesOpensRepository sesOpensRepository,
ILogger<NotificationService> logger)
{
_notificationsRepository = notificationsRepository;
_sesBouncesRepository = sesBouncesRepository;
_sesComplaintsRepository = sesComplaintsRepository;
_sesDeliveriesRepository = sesDeliveriesRepository;
_sesOpensRepository = sesOpensRepository;
_logger = logger;
}

Expand All @@ -52,19 +56,43 @@ public void HandleNotification(string content)
private void HandleNotificationInternal(string content)
{
var ses = JsonConvert.DeserializeObject<Ses>(content);
switch (ses.NotificationType.ToLower())

if (ses == null || (string.IsNullOrEmpty(ses.NotificationType) && string.IsNullOrEmpty(ses.EventType)))
{
throw new NotSupportedException($"Unsupported message {content.Substring(0, 50)}...");
}

if (!string.IsNullOrEmpty(ses.NotificationType))
{
switch (ses.NotificationType.ToLower())
{
case Delivery:
HandleDelivery(content);
break;
case Complaint:
HandleComplaint(content);
break;
case Bounce:
HandleBounce(content);
break;
case Open:
HandleOpen(content);
break;
default:
throw new NotSupportedException($"Unsupported message {content.Substring(0, 50)}...");
}
}

if (!string.IsNullOrEmpty(ses.EventType))
{
case Delivery:
HandleDelivery(content);
break;
case Complaint:
HandleComplaint(content);
break;
case Bounce:
HandleBounce(content);
break;
default:
throw new NotSupportedException($"Unsupported notification type {ses.NotificationType}");
switch (ses.EventType.ToLower())
{
case Open:
HandleOpen(content);
break;
default:
throw new NotSupportedException($"Unsupported message {content.Substring(0, 50)}...");
}
}
}

Expand Down Expand Up @@ -95,6 +123,15 @@ private void HandleBounce(string content)
_sesBouncesRepository.Save(bounce.Create(notification.Id));
}

private void HandleOpen(string content)
{
var open = JsonConvert.DeserializeObject<SesOpenModel>(content);

var notification = SaveNotification(open.Mail, content);

_sesOpensRepository.Save(open.Create(notification.Id));
}

private SesNotification SaveNotification(SesMail mail, string content)
{
var notification = mail.Create(content);
Expand Down
12 changes: 12 additions & 0 deletions Projects/SesNotifications.DataAccess/Entities/SesOpen.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace SesNotifications.DataAccess.Entities
{
public class SesOpen : SesCommon
{
public virtual string Recipients { get; set; }
public virtual DateTime OpenedAt { get; set; }
public virtual string UserAgent { get; set; }
public virtual string IpAddress { get; set; }
}
}
17 changes: 17 additions & 0 deletions Projects/SesNotifications.DataAccess/Mappings/SesOpenMap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using SesNotifications.DataAccess.Entities;

namespace SesNotifications.DataAccess.Mappings
{
public class SesOpenMap : SesCommonMap<SesOpen>
{
public SesOpenMap()
{
Table("ses_notifications.opens");
MapCommon();
Map(x => x.Recipients).Column("recipients");
Map(x => x.OpenedAt).Column("opened_at");
Map(x => x.UserAgent).Column("user_agent");
Map(x => x.IpAddress).Column("ip_address");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using SesNotifications.DataAccess.Entities;

namespace SesNotifications.DataAccess.Repositories.Interfaces
{
public interface ISesOpensRepository
{
void Save(SesOpen sesOpen);
SesOpen FindById(long id);
IList<SesOpen> FindByMessageId(string messageId);
IList<SesOpen> FindBySentDateRange(DateTime start, DateTime end);
IList<SesOpen> FindByRecipient(string email);
IList<SesOpen> FindByRecipientAndSentDateRange(string email, DateTime start, DateTime end);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using NHibernate;
using NHibernate.Criterion;
using SesNotifications.DataAccess.Entities;
using SesNotifications.DataAccess.Repositories.Interfaces;

namespace SesNotifications.DataAccess.Repositories
{
public class SesOpensRepository : Repository, ISesOpensRepository
{

public SesOpensRepository()
{
}

public SesOpensRepository(ISession session) : base(session)
{
}

public void Save(SesOpen sesDelivery)
{
Session.Save(sesDelivery);
}

public SesOpen FindById(long id)
{
return Session.Get<SesOpen>(id);
}

public IList<SesOpen> FindByMessageId(string messageId)
{
return Session.CreateCriteria<SesOpen>()
.Add(Restrictions.Eq(nameof(SesOpen.MessageId), messageId))
.List<SesOpen>();
}

public IList<SesOpen> FindBySentDateRange(DateTime start, DateTime end)
{
return Session.CreateCriteria<SesOpen>()
.Add(Restrictions.Ge(nameof(SesOpen.SentAt), start))
.Add(Restrictions.Le(nameof(SesOpen.SentAt), end))
.AddOrder(Order.Desc(nameof(SesOpen.SentAt)))
.List<SesOpen>();
}

public IList<SesOpen> FindByRecipient(string email)
{
return Session.CreateCriteria<SesOpen>()
.Add(Restrictions.InsensitiveLike(nameof(SesOpen.Recipients), email))
.AddOrder(Order.Desc(nameof(SesOpen.SentAt)))
.List<SesOpen>();
}

public IList<SesOpen> FindByRecipientAndSentDateRange(string email, DateTime start, DateTime end)
{
return Session.CreateCriteria<SesOpen>()
.Add(Restrictions.InsensitiveLike(nameof(SesOpen.Recipients), email))
.Add(Restrictions.Ge(nameof(SesOpen.SentAt), start))
.Add(Restrictions.Le(nameof(SesOpen.SentAt), end))
.AddOrder(Order.Desc(nameof(SesOpen.SentAt)))
.List<SesOpen>();
}

}
}
5 changes: 4 additions & 1 deletion Projects/SesNotifications.DataAccess/StartupExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using FluentNHibernate.Cfg.Db;
using Microsoft.Extensions.DependencyInjection;
using NHibernate.Dialect;
using SesNotifications.DataAccess.Entities;
using SesNotifications.DataAccess.Mappings;
using SesNotifications.DataAccess.Repositories;
using SesNotifications.DataAccess.Repositories.Interfaces;
Expand All @@ -20,7 +21,8 @@ public static IServiceCollection AddNHibernate(this IServiceCollection services,
.Add<SesDeliveryMap>()
.Add<SesComplaintMap>()
.Add<SesNotificationMap>()
.Add<SesBounceMap>())
.Add<SesBounceMap>()
.Add<SesOpenMap>())
.BuildSessionFactory();

SessionManager.Build(sessionFactory);
Expand All @@ -42,6 +44,7 @@ public static IServiceCollection AddScopedRepositories(this IServiceCollection s
services.AddScoped<ISesBouncesRepository, SesBouncesRepository>();
services.AddScoped<ISesDeliveriesRepository, SesDeliveriesRepository>();
services.AddScoped<ISesComplaintsRepository, SesComplaintsRepository>();
services.AddScoped<ISesOpensRepository, SesOpensRepository>();

return services;
}
Expand Down
22 changes: 22 additions & 0 deletions Sql/ses_notifications_init.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,26 @@
CREATE SCHEMA ses_notifications AUTHORIZATION postgres;
CREATE TABLE ses_notifications.opens (
id int8 NOT NULL GENERATED ALWAYS AS IDENTITY,
notification_id int8 NOT NULL,
notification_type varchar(32) NOT NULL,
sent_at timestamptz NOT NULL,
message_id varchar(128) NOT NULL,
source varchar(256) NOT NULL COLLATE "ucs_basic",
source_arn varchar(256) NULL,
source_ip varchar(32) NULL,
sending_account_id varchar(128) NULL,
recipients varchar(64000) NULL COLLATE "ucs_basic",
opened_at timestamptz NULL,
user_agent varchar(1024) NULL,
ip_address varchar(32) NULL
);
CREATE INDEX opens_recipients_idx ON ses_notifications.opens USING btree (recipients);
CREATE INDEX opens_from_idx ON ses_notifications.opens USING btree (source);
CREATE UNIQUE INDEX opens_id_idx ON ses_notifications.opens USING btree (id);
CREATE INDEX opens_message_id_idx ON ses_notifications.opens USING btree (message_id);
CREATE INDEX opens_notification_id_idx ON ses_notifications.opens USING btree (notification_id);
CREATE INDEX opens_sent_at_idx ON ses_notifications.opens USING btree (sent_at);

CREATE TABLE ses_notifications.bounces (
id int8 NOT NULL GENERATED ALWAYS AS IDENTITY,
notification_id int8 NOT NULL,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using SesNotifications.App.Factories;
using SesNotifications.App.Tests.Helpers;
using Xunit;

namespace SesNotifications.App.Tests.Factories
{
public class DbSesOpenFactoryTests
{
[Fact]
public void Verify()
{
var dt = DateTime.UtcNow;
var open = TestHelpers.GetSesOpenModel(dt);

var sesOpen = open.Create(1);

Assert.Equal(sesOpen.Recipients, string.Join(',', open.Mail.Destination));
Assert.Equal(sesOpen.OpenedAt.Iso8601(), open.Open.Timestamp);
Assert.Equal(sesOpen.SourceArn, open.Mail.SourceArn);
Assert.Equal(sesOpen.NotificationType, open.NotificationType);
Assert.Equal(1, sesOpen.NotificationId);
Assert.Equal(sesOpen.MessageId, open.Mail.MessageId);
}
}
}
15 changes: 15 additions & 0 deletions Tests/SesNotifications.App.Tests/Helpers/TestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ public static SesBounceModel GetSesBounceModel(DateTime dt)
};
}

public static SesOpenModel GetSesOpenModel(DateTime dt)
{
return new SesOpenModel
{
NotificationType = "Open",
Mail = GetSesMail(dt),
Open = new SesOpen
{
UserAgent = "user_agent",
IpAddress = "ip_address",
Timestamp = dt.Iso8601()
}
};
}

public static SesMail GetSesMail(DateTime dt)
{
return new SesMail
Expand Down
Loading

0 comments on commit 5de543a

Please sign in to comment.