aboutsummaryrefslogtreecommitdiff
path: root/lib/Emails.Transactional.Plugin/src/Endpoints/SendEndpoint.cs
blob: 3e6f94020dcd856fd67e6580c51929d93196bcd6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
/*
* Copyright (c) 2023 Vaughn Nugent
* 
* Library: VNLib
* Package: Emails.Transactional
* File: SendEndpoint.cs 
*
* SendEndpoint.cs is part of Emails.Transactional which is part of the larger 
* VNLib collection of libraries and utilities.
*
* Emails.Transactional is free software: you can redistribute it and/or modify 
* it under the terms of the GNU General Public License as published
* by the Free Software Foundation, either version 2 of the License,
* or (at your option) any later version.
*
* Emails.Transactional is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License 
* along with Emails.Transactional. If not, see http://www.gnu.org/licenses/.
*/


using System;
using System.Net;
using System.Threading.Tasks;

using VNLib.Utils.Logging;
using VNLib.Plugins;
using VNLib.Plugins.Essentials;
using VNLib.Plugins.Essentials.Extensions;
using VNLib.Plugins.Essentials.Oauth;
using VNLib.Plugins.Extensions.Validation;
using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Extensions.Loading.Sql;
using VNLib.Plugins.Extensions.Data.Extensions;

using Emails.Transactional.Mta;
using Emails.Transactional.Templates;
using Emails.Transactional.Transactions;

namespace Emails.Transactional.Endpoints
{

    [ConfigurationName("send_endpoint")]
    internal class SendEndpoint : O2EndpointBase
    {
        public const string OAUTH2_USER_SCOPE_PERMISSION = "email:send";

        private static readonly EmailTransactionValidator Validator = new();

        private readonly string FromAddress;
       
        private readonly TransactionStore Transactions;
        private readonly IMailTransferAgent EmailService;
        private readonly ITemplateStorage Templates;

        public SendEndpoint(PluginBase plugin, IConfigScope config)
        {
            string? path = config["path"].GetString();
            FromAddress = config.GetRequiredProperty("from_address", e => e.GetString()!);

            InitPathAndLog(path, plugin.Log);

            //Load transactions
            Transactions = new(plugin.GetContextOptions());

            //init ail transfer agent
            EmailService = plugin.GetOrCreateSingleton<MailTransferAgent>();

            //Load templates
            Templates = plugin.GetOrCreateSingleton<EmailTemplateStore>();
        }

        protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity)
        {
            //Make sure the user has the required scope to send an email
            if (!entity.Session.HasScope(OAUTH2_USER_SCOPE_PERMISSION))
            {
                entity.CloseResponseError(HttpStatusCode.Forbidden, ErrorType.InvalidScope, "Your account does not have the required permissions scope");
                return VfReturnType.VirtualSkip;
            }

            //Get the transaction request from the client
            EmailTransaction? transaction = await entity.GetJsonFromFileAsync<EmailTransaction>();

            if(transaction == null)
            {
                entity.CloseResponseError(HttpStatusCode.BadRequest, ErrorType.InvalidRequest, "No request body was received");
                return VfReturnType.VirtualSkip;
            }
                        
            TransactionResult webm = new()
            {
                Success = false
            };

            //Set a default from name/address if not set by user
            transaction.From ??= FromAddress;
            transaction.FromName ??= FromAddress;

            //Specify user-id
            transaction.UserId = entity.Session.UserID;

            //validate the form
            if (!Validator.Validate(transaction, webm))
            {
                //return errors
                entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm);
                return VfReturnType.VirtualSkip;
            }

            //Always add the template name to the model
            transaction.Variables!["template_name"] = transaction.TemplateId!;

            try
            {
                //Get the template
                IEmailTemplate template = await Templates.GetTemplateAsync(transaction.TemplateId!, entity.EventCancellation);

                //Render the template with the model
                IEmailMessageData message = template.RenderTemplate(transaction.Variables!);

                //Send the template
                MtaResult result = await EmailService.SendEmailAsync(transaction, message, entity.EventCancellation);

                //Set success flag
                webm.Success = result.Success;
                webm.SmtpStatus = result.Message;
            }
            //Handle a template lookup failure
            catch(TemplateLookupFailedException ex)
            {
                Log.Debug("Failed to fetch template: {err}", transaction.TemplateId, ex.Message);

                entity.CloseResponseError(HttpStatusCode.NotFound, ErrorType.InvalidRequest, "The requested template does not exist");
                return VfReturnType.VirtualSkip;
            }
            catch (Exception ex)
            {
                Log.Error(ex);

                //Write an error status message to the transaction store
                webm.SmtpStatus = transaction.Result = ex.Message;
            }

            //Write transaction to db (we need to return the transaction id)
            _ = await Transactions.AddOrUpdateAsync(transaction, entity.EventCancellation);

            //Store the results object
            webm.TransactionId = transaction.Id;

            //Return the response object
            entity.CloseResponse(webm);
            return VfReturnType.VirtualSkip;
        }
    }
}