mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-10 06:08:42 +02:00
recipient manual send action
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -134,9 +134,10 @@ const (
|
||||
ROUTE_V1_CAMPAIGN_STATS_ALL = "/api/v1/campaign/stats/all"
|
||||
ROUTE_V1_CAMPAIGN_UPLOAD_REPORTED = "/api/v1/campaign/:id/upload/reported"
|
||||
// campaign-recipient
|
||||
ROUTE_V1_CAMPAIGN_RECIPIENT_EMAIL = "/api/v1/campaign/recipient/:id/email"
|
||||
ROUTE_V1_CAMPAIGN_RECIPIENT_URL = "/api/v1/campaign/recipient/:id/url"
|
||||
ROUTE_V1_CAMPAIGN_RECIPIENT_SET_SENT = "/api/v1/campaign/recipient/:id/sent"
|
||||
ROUTE_V1_CAMPAIGN_RECIPIENT_EMAIL = "/api/v1/campaign/recipient/:id/email"
|
||||
ROUTE_V1_CAMPAIGN_RECIPIENT_URL = "/api/v1/campaign/recipient/:id/url"
|
||||
ROUTE_V1_CAMPAIGN_RECIPIENT_SET_SENT = "/api/v1/campaign/recipient/:id/sent"
|
||||
ROUTE_V1_CAMPAIGN_RECIPIENT_SEND_EMAIL = "/api/v1/campaign/recipient/:id/send"
|
||||
// asset
|
||||
ROUTE_V1_ASSET = "/api/v1/asset"
|
||||
ROUTE_V1_ASSET_ID = "/api/v1/asset/:id"
|
||||
@@ -373,6 +374,7 @@ func setupRoutes(
|
||||
GET(ROUTE_V1_CAMPAIGN_RECIPIENT_EMAIL, middleware.SessionHandler, controllers.Campaign.GetCampaignEmail).
|
||||
GET(ROUTE_V1_CAMPAIGN_RECIPIENT_URL, middleware.SessionHandler, controllers.Campaign.GetCampaignURL).
|
||||
POST(ROUTE_V1_CAMPAIGN_RECIPIENT_SET_SENT, middleware.SessionHandler, controllers.Campaign.SetSentAtByCampaignRecipientID).
|
||||
POST(ROUTE_V1_CAMPAIGN_RECIPIENT_SEND_EMAIL, middleware.SessionHandler, controllers.Campaign.SendEmailByCampaignRecipientID).
|
||||
// asset
|
||||
GET(ROUTE_V1_ASSET_DOMAIN_VIEW, middleware.SessionHandler, controllers.Asset.GetContentByID).
|
||||
GET(ROUTE_V1_ASSET_ID, middleware.SessionHandler, controllers.Asset.GetByID).
|
||||
|
||||
@@ -854,6 +854,27 @@ func (c *Campaign) SetSentAtByCampaignRecipientID(g *gin.Context) {
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// SendEmailByCampaignRecipientID sends an email to a specific campaign recipient
|
||||
func (c *Campaign) SendEmailByCampaignRecipientID(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse request
|
||||
id, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// send email
|
||||
err := c.CampaignService.SendEmailByCampaignRecipientID(g.Request.Context(), session, id)
|
||||
// handle responses
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// DeleteByID deletes a campaign by its id
|
||||
func (c *Campaign) DeleteByID(g *gin.Context) {
|
||||
// handle session
|
||||
|
||||
@@ -2961,6 +2961,447 @@ func (c *Campaign) AnonymizeByID(
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendEmailByCampaignRecipientID sends an email to a specific campaign recipient
|
||||
// Multiple sends to the same recipient are allowed to support retry scenarios and follow-ups.
|
||||
func (c *Campaign) SendEmailByCampaignRecipientID(
|
||||
ctx context.Context,
|
||||
session *model.Session,
|
||||
campaignRecipientID *uuid.UUID,
|
||||
) error {
|
||||
ae := NewAuditEvent("Campaign.SendEmailByCampaignRecipientID", session)
|
||||
ae.Details["campaignRecipientId"] = campaignRecipientID.String()
|
||||
// check permissions
|
||||
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
||||
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
||||
c.LogAuthError(err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
if !isAuthorized {
|
||||
c.AuditLogNotAuthorized(ae)
|
||||
return errs.ErrAuthorizationFailed
|
||||
}
|
||||
|
||||
// get campaign recipient
|
||||
campaignRecipient, err := c.CampaignRecipientRepository.GetByID(
|
||||
ctx,
|
||||
campaignRecipientID,
|
||||
&repository.CampaignRecipientOption{
|
||||
WithRecipient: true,
|
||||
WithCampaign: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to get campaign recipient by id", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
campaign := campaignRecipient.Campaign
|
||||
if campaign == nil {
|
||||
return errors.New("campaign recipient has no campaign loaded")
|
||||
}
|
||||
|
||||
// check if campaign is active
|
||||
if !campaign.IsActive() {
|
||||
return errors.New("campaign is not active")
|
||||
}
|
||||
|
||||
// check if recipient exists (not anonymized)
|
||||
if campaignRecipient.Recipient == nil {
|
||||
return errors.New("recipient is anonymized or deleted")
|
||||
}
|
||||
|
||||
// check if cancelled
|
||||
if !campaignRecipient.CancelledAt.IsNull() {
|
||||
return errors.New("recipient has been cancelled")
|
||||
}
|
||||
|
||||
campaignID := campaign.ID.MustGet()
|
||||
|
||||
// add resend information to audit log
|
||||
isResend := !campaignRecipient.SentAt.IsNull()
|
||||
ae.Details["isResend"] = isResend
|
||||
if isResend {
|
||||
ae.Details["previouslySentAt"] = campaignRecipient.SentAt.MustGet().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// send the email using existing logic from sendCampaignMessages
|
||||
err = c.sendSingleCampaignMessage(ctx, session, &campaignID, campaignRecipient)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to send campaign message", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
c.AuditLogAuthorized(ae)
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendSingleCampaignMessage sends an email to a single campaign recipient
|
||||
func (c *Campaign) sendSingleCampaignMessage(
|
||||
ctx context.Context,
|
||||
session *model.Session,
|
||||
campaignID *uuid.UUID,
|
||||
campaignRecipient *model.CampaignRecipient,
|
||||
) error {
|
||||
// get campaign template details - similar logic from sendCampaignMessages
|
||||
campaign, err := c.CampaignRepository.GetByID(
|
||||
ctx,
|
||||
campaignID,
|
||||
&repository.CampaignOption{},
|
||||
)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to get campaign by id", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
templateID, err := campaign.TemplateID.Get()
|
||||
if err != nil {
|
||||
return errors.New("campaign has no template")
|
||||
}
|
||||
|
||||
cTemplate, err := c.CampaignTemplateService.GetByID(
|
||||
ctx,
|
||||
session,
|
||||
&templateID,
|
||||
&repository.CampaignTemplateOption{
|
||||
WithDomain: true,
|
||||
WithSMTPConfiguration: true,
|
||||
WithIdentifier: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to get campaign template by id", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
// check domain
|
||||
domain := cTemplate.Domain
|
||||
if domain == nil {
|
||||
return errors.New("campaign template has no domain")
|
||||
}
|
||||
|
||||
// get email details
|
||||
emailID, err := cTemplate.EmailID.Get()
|
||||
if err != nil {
|
||||
return errors.New("campaign template has no email")
|
||||
}
|
||||
|
||||
email, err := c.MailService.GetByID(ctx, session, &emailID)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to get email by id", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
// update last attempt timestamp
|
||||
campaignRecipientID := campaignRecipient.ID.MustGet()
|
||||
campaignRecipient.LastAttemptAt = nullable.NewNullableWithValue(time.Now())
|
||||
err = c.CampaignRecipientRepository.UpdateByID(ctx, &campaignRecipientID, campaignRecipient)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to update last attempted at", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
// prepare template for rendering
|
||||
content, err := email.Content.Get()
|
||||
if err != nil {
|
||||
return errors.New("failed to get email content")
|
||||
}
|
||||
|
||||
t := template.New("email")
|
||||
t = t.Funcs(TemplateFuncs())
|
||||
mailTmpl, err := t.Parse(content.String())
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
// check sending method
|
||||
isSmtpCampaign := cTemplate.SMTPConfigurationID.IsSpecified() && !cTemplate.SMTPConfigurationID.IsNull()
|
||||
isAPISenderCampaign := cTemplate.APISenderID.IsSpecified() && !cTemplate.APISenderID.IsNull()
|
||||
|
||||
if !isSmtpCampaign && !isAPISenderCampaign {
|
||||
return errors.New("campaign template has no SMTP configuration or API sender")
|
||||
}
|
||||
|
||||
if isAPISenderCampaign {
|
||||
// send via API
|
||||
err = c.APISenderService.Send(
|
||||
ctx,
|
||||
session,
|
||||
cTemplate,
|
||||
campaignRecipient,
|
||||
domain,
|
||||
mailTmpl,
|
||||
email,
|
||||
)
|
||||
} else {
|
||||
// send via SMTP
|
||||
err = c.sendSingleEmailSMTP(ctx, session, cTemplate, campaignRecipient, domain, mailTmpl, email)
|
||||
}
|
||||
|
||||
// save sending result
|
||||
saveErr := c.saveSendingResult(ctx, campaignRecipient, err)
|
||||
if saveErr != nil {
|
||||
c.Logger.Errorw("failed to save sending result", "error", saveErr)
|
||||
return errs.Wrap(saveErr)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// sendSingleEmailSMTP sends an email to a single recipient via SMTP
|
||||
func (c *Campaign) sendSingleEmailSMTP(
|
||||
ctx context.Context,
|
||||
session *model.Session,
|
||||
cTemplate *model.CampaignTemplate,
|
||||
campaignRecipient *model.CampaignRecipient,
|
||||
domain *model.Domain,
|
||||
mailTmpl *template.Template,
|
||||
email *model.Email,
|
||||
) error {
|
||||
// get SMTP configuration
|
||||
smtpConfigID, err := cTemplate.SMTPConfigurationID.Get()
|
||||
if err != nil {
|
||||
return errors.New("failed to get SMTP configuration from template")
|
||||
}
|
||||
|
||||
smtpConfig, err := c.SMTPConfigService.GetByID(
|
||||
ctx,
|
||||
session, // use the actual session passed to the method
|
||||
&smtpConfigID,
|
||||
&repository.SMTPConfigurationOption{
|
||||
WithHeaders: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("smtp configuration did not load", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
smtpPort, err := smtpConfig.Port.Get()
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
smtpHost, err := smtpConfig.Host.Get()
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
smtpIgnoreCertErrors, err := smtpConfig.IgnoreCertErrors.Get()
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
// setup SMTP client options
|
||||
emailOptions := []mail.Option{
|
||||
mail.WithPort(smtpPort.Int()),
|
||||
mail.WithTLSConfig(
|
||||
&tls.Config{
|
||||
ServerName: smtpHost.String(),
|
||||
InsecureSkipVerify: smtpIgnoreCertErrors,
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
// setup authentication if provided
|
||||
username, err := smtpConfig.Username.Get()
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
password, err := smtpConfig.Password.Get()
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
if un := username.String(); len(un) > 0 {
|
||||
emailOptions = append(emailOptions, mail.WithUsername(un))
|
||||
if pw := password.String(); len(pw) > 0 {
|
||||
emailOptions = append(emailOptions, mail.WithPassword(pw))
|
||||
}
|
||||
}
|
||||
|
||||
// create message
|
||||
messageOptions := []mail.MsgOption{
|
||||
mail.WithNoDefaultUserAgent(),
|
||||
}
|
||||
m := mail.NewMsg(messageOptions...)
|
||||
|
||||
// set envelope from
|
||||
err = m.EnvelopeFrom(email.MailEnvelopeFrom.MustGet().String())
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to set envelope from", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
// set headers
|
||||
err = m.From(email.MailHeaderFrom.MustGet().String())
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to set mail header 'From'", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
recpEmail := campaignRecipient.Recipient.Email.MustGet().String()
|
||||
err = m.To(recpEmail)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to set mail header 'To'", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
// custom headers
|
||||
if headers := smtpConfig.Headers; headers != nil {
|
||||
for _, header := range headers {
|
||||
key := header.Key.MustGet()
|
||||
value := header.Value.MustGet()
|
||||
m.SetGenHeader(
|
||||
mail.Header(key.String()),
|
||||
value.String(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
m.Subject(email.MailHeaderSubject.MustGet().String())
|
||||
|
||||
// setup template variables
|
||||
domainName, err := domain.Name.Get()
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
urlIdentifier := cTemplate.URLIdentifier
|
||||
if urlIdentifier == nil {
|
||||
return errors.New("url identifier must be loaded for the campaign template")
|
||||
}
|
||||
|
||||
urlPath := cTemplate.URLPath.MustGet().String()
|
||||
t := c.TemplateService.CreateMail(
|
||||
domainName.String(),
|
||||
urlIdentifier.Name.MustGet(),
|
||||
urlPath,
|
||||
campaignRecipient,
|
||||
email,
|
||||
nil,
|
||||
)
|
||||
|
||||
err = m.SetBodyHTMLTemplate(mailTmpl, t)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to set body html template", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
// handle attachments
|
||||
attachments := email.Attachments
|
||||
for _, attachment := range attachments {
|
||||
p, err := c.MailService.AttachmentService.GetPath(attachment)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get attachment path: %s", err)
|
||||
}
|
||||
if !attachment.EmbeddedContent.MustGet() {
|
||||
m.AttachFile(p.String())
|
||||
} else {
|
||||
attachmentContent, err := os.ReadFile(p.String())
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
// setup attachment for executing as email template
|
||||
attachmentAsEmail := model.Email{
|
||||
ID: email.ID,
|
||||
CreatedAt: email.CreatedAt,
|
||||
UpdatedAt: email.UpdatedAt,
|
||||
Name: email.Name,
|
||||
MailEnvelopeFrom: email.MailEnvelopeFrom,
|
||||
MailHeaderFrom: email.MailHeaderFrom,
|
||||
MailHeaderSubject: email.MailHeaderSubject,
|
||||
Content: email.Content,
|
||||
AddTrackingPixel: email.AddTrackingPixel,
|
||||
CompanyID: email.CompanyID,
|
||||
Attachments: email.Attachments,
|
||||
Company: email.Company,
|
||||
}
|
||||
attachmentAsEmail.Content = nullable.NewNullableWithValue(
|
||||
*vo.NewUnsafeOptionalString1MB(string(attachmentContent)),
|
||||
)
|
||||
attachmentStr, err := c.TemplateService.CreateMailBody(
|
||||
urlIdentifier.Name.MustGet(),
|
||||
urlPath,
|
||||
domain,
|
||||
campaignRecipient,
|
||||
&attachmentAsEmail,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return errs.Wrap(fmt.Errorf("failed to setup attachment with embedded content: %s", err))
|
||||
}
|
||||
m.AttachReadSeeker(
|
||||
filepath.Base(p.String()),
|
||||
strings.NewReader(attachmentStr),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// send the email
|
||||
var mc *mail.Client
|
||||
|
||||
// try different authentication methods
|
||||
if un := username.String(); len(un) > 0 {
|
||||
// try CRAM-MD5 first when credentials are provided
|
||||
emailOptionsCRAM5 := append(emailOptions, mail.WithSMTPAuth(mail.SMTPAuthCramMD5))
|
||||
mc, _ = mail.NewClient(smtpHost.String(), emailOptionsCRAM5...)
|
||||
mc.SetLogger(log.NewGoMailLoggerAdapter(c.Logger))
|
||||
mc.SetDebugLog(true)
|
||||
if build.Flags.Production {
|
||||
mc.SetTLSPolicy(mail.TLSMandatory)
|
||||
} else {
|
||||
mc.SetTLSPolicy(mail.TLSOpportunistic)
|
||||
}
|
||||
err = mc.DialAndSendWithContext(ctx, m)
|
||||
|
||||
// check if it's an authentication error and try PLAIN auth
|
||||
if err != nil && (strings.Contains(err.Error(), "535 ") ||
|
||||
strings.Contains(err.Error(), "534 ") ||
|
||||
strings.Contains(err.Error(), "538 ") ||
|
||||
strings.Contains(err.Error(), "CRAM-MD5") ||
|
||||
strings.Contains(err.Error(), "authentication failed")) {
|
||||
c.Logger.Debugf("CRAM-MD5 authentication failed, trying PLAIN auth", "error", err)
|
||||
emailOptionsBasic := emailOptions
|
||||
emailOptionsBasic = append(emailOptions, mail.WithSMTPAuth(mail.SMTPAuthPlain))
|
||||
mc, _ = mail.NewClient(smtpHost.String(), emailOptionsBasic...)
|
||||
mc.SetLogger(log.NewGoMailLoggerAdapter(c.Logger))
|
||||
mc.SetDebugLog(true)
|
||||
if build.Flags.Production {
|
||||
mc.SetTLSPolicy(mail.TLSMandatory)
|
||||
} else {
|
||||
mc.SetTLSPolicy(mail.TLSOpportunistic)
|
||||
}
|
||||
err = mc.DialAndSendWithContext(ctx, m)
|
||||
}
|
||||
} else {
|
||||
// no credentials provided, try without authentication
|
||||
mc, _ = mail.NewClient(smtpHost.String(), emailOptions...)
|
||||
mc.SetLogger(log.NewGoMailLoggerAdapter(c.Logger))
|
||||
mc.SetDebugLog(true)
|
||||
if build.Flags.Production {
|
||||
mc.SetTLSPolicy(mail.TLSMandatory)
|
||||
} else {
|
||||
mc.SetTLSPolicy(mail.TLSOpportunistic)
|
||||
}
|
||||
err = mc.DialAndSendWithContext(ctx, m)
|
||||
|
||||
// if no-auth fails and we get an auth-related error, log it appropriately
|
||||
if err != nil && (strings.Contains(err.Error(), "530 ") ||
|
||||
strings.Contains(err.Error(), "535 ") ||
|
||||
strings.Contains(err.Error(), "authentication required") ||
|
||||
strings.Contains(err.Error(), "AUTH")) {
|
||||
c.Logger.Warnw("Server requires authentication but no credentials provided", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to send email", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetNotableCampaignEvent checks and update if most notable event for a campaign
|
||||
func (c *Campaign) setMostNotableCampaignEvent(
|
||||
ctx context.Context,
|
||||
|
||||
@@ -689,6 +689,16 @@ export class API {
|
||||
return await postJSON(this.getPath(`/campaign/recipient/${campaignRecipient}/sent`));
|
||||
},
|
||||
|
||||
/**
|
||||
* Send email to campaign recipient
|
||||
*
|
||||
* @param {string} campaignRecipientID
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
sendEmail: async (campaignRecipientID) => {
|
||||
return await postJSON(this.getPath(`/campaign/recipient/${campaignRecipientID}/send`));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get campaign recipient landingpage URL.
|
||||
*
|
||||
|
||||
@@ -121,6 +121,8 @@
|
||||
let isRecipientTableLoading = false;
|
||||
let isCloseModalVisible = false;
|
||||
let isAnonymizeModalVisible = false;
|
||||
let isSendEmailModalVisible = false;
|
||||
let sendEmailRecipient = null;
|
||||
let lastPoll3399Nano = '';
|
||||
|
||||
// hooks
|
||||
@@ -489,6 +491,50 @@
|
||||
}
|
||||
};
|
||||
|
||||
/** @param {string} campaignRecipientID @param {Object} recipient */
|
||||
const showSendEmailModal = (campaignRecipientID, recipient) => {
|
||||
sendEmailRecipient = {
|
||||
id: campaignRecipientID,
|
||||
name: `${recipient.firstName || ''} ${recipient.lastName || ''}`.trim(),
|
||||
email: recipient.email
|
||||
};
|
||||
isSendEmailModalVisible = true;
|
||||
};
|
||||
|
||||
const closeSendEmailModal = () => {
|
||||
isSendEmailModalVisible = false;
|
||||
sendEmailRecipient = null;
|
||||
};
|
||||
|
||||
const onConfirmSendEmail = async () => {
|
||||
try {
|
||||
showIsLoading();
|
||||
// Check if this is a resend before sending
|
||||
const isResend = campaignRecipients.find((r) => r.id === sendEmailRecipient.id)?.sentAt;
|
||||
const res = await api.campaign.sendEmail(sendEmailRecipient.id);
|
||||
if (!res.success) {
|
||||
throw res.error;
|
||||
}
|
||||
const campaignType = isSelfManaged ? 'self-managed' : 'scheduled';
|
||||
const message = isResend ? `Email sent again successfully` : `Email sent successfully`;
|
||||
addToast(message, 'Success');
|
||||
await setCampaign();
|
||||
await getEvents();
|
||||
await refreshCampaignRecipients();
|
||||
closeSendEmailModal();
|
||||
} catch (e) {
|
||||
addToast('Failed to send email', 'Error');
|
||||
console.error('failed to send email', e);
|
||||
} finally {
|
||||
hideIsLoading();
|
||||
}
|
||||
};
|
||||
|
||||
// reactive statement to clean up send email modal state when it closes
|
||||
$: if (!isSendEmailModalVisible && sendEmailRecipient) {
|
||||
sendEmailRecipient = null;
|
||||
}
|
||||
|
||||
const showCloseCampaignModal = () => {
|
||||
isCloseModalVisible = true;
|
||||
};
|
||||
@@ -1412,6 +1458,16 @@
|
||||
disabled={!recp.recipient}
|
||||
on:click={() => openEventsModal(recp.recipientID)}
|
||||
/>
|
||||
<TableDropDownButton
|
||||
name={recp.sentAt ? 'Send email again' : 'Send email'}
|
||||
title={recp.closedAt
|
||||
? 'Campaign is closed'
|
||||
: recp.cancelledAt
|
||||
? 'Recipient cancelled'
|
||||
: 'Send email'}
|
||||
on:click={() => showSendEmailModal(recp.id, recp.recipient)}
|
||||
disabled={!!campaign.closedAt || recp.cancelledAt}
|
||||
/>
|
||||
{#if !campaign.sendStartAt}
|
||||
<!-- self managed campaign -->
|
||||
<TableDropDownButton
|
||||
@@ -1698,4 +1754,38 @@
|
||||
</ul>
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<Alert
|
||||
headline="Send Email"
|
||||
bind:visible={isSendEmailModalVisible}
|
||||
onConfirm={onConfirmSendEmail}
|
||||
>
|
||||
<div>
|
||||
{#if sendEmailRecipient}
|
||||
{@const recipient = campaignRecipients.find((r) => r.id === sendEmailRecipient.id)}
|
||||
{#if recipient}
|
||||
<p class="mb-4">
|
||||
{recipient.sentAt
|
||||
? 'Are you sure you want to send the campaign email again to:'
|
||||
: 'Are you sure you want to send the campaign email to:'}
|
||||
</p>
|
||||
<div class="bg-gray-50 p-3 rounded mb-4">
|
||||
<p class="font-medium">{sendEmailRecipient.name}</p>
|
||||
<p class="text-gray-600">{sendEmailRecipient.email}</p>
|
||||
<p class="text-xs text-blue-600 mt-1">
|
||||
Campaign type: {isSelfManaged ? 'Self-managed' : 'Scheduled'}
|
||||
</p>
|
||||
{#if recipient.sentAt}
|
||||
<p class="text-sm text-amber-600 mt-1">
|
||||
⚠️ Previously sent on {new Date(recipient.sentAt).toLocaleString()}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-sm text-gray-500">
|
||||
This action will immediately send the email and cannot be undone.
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</Alert>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user