Merge pull request #2315 from omnivore-app/fix/unsubscribe-email-address
fix: parse unsubscribe mailto email address and set subject
This commit is contained in:
@ -1,4 +1,8 @@
|
||||
import { authorized } from '../../utils/helpers'
|
||||
import { ILike } from 'typeorm'
|
||||
import { Subscription } from '../../entity/subscription'
|
||||
import { User } from '../../entity/user'
|
||||
import { getRepository } from '../../entity/utils'
|
||||
import { env } from '../../env'
|
||||
import {
|
||||
MutationSubscribeArgs,
|
||||
MutationUnsubscribeArgs,
|
||||
@ -16,13 +20,9 @@ import {
|
||||
UnsubscribeErrorCode,
|
||||
UnsubscribeSuccess,
|
||||
} from '../../generated/graphql'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
import { env } from '../../env'
|
||||
import { getRepository } from '../../entity/utils'
|
||||
import { User } from '../../entity/user'
|
||||
import { Subscription } from '../../entity/subscription'
|
||||
import { getSubscribeHandler, unsubscribe } from '../../services/subscriptions'
|
||||
import { ILike } from 'typeorm'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
import { authorized } from '../../utils/helpers'
|
||||
import { createImageProxyUrl } from '../../utils/imageproxy'
|
||||
|
||||
export const subscriptionsResolver = authorized<
|
||||
@ -90,10 +90,13 @@ export const unsubscribeResolver = authorized<
|
||||
}
|
||||
}
|
||||
|
||||
const subscription = await getRepository(Subscription).findOne({
|
||||
where: { name: ILike(name), user: { id: uid } },
|
||||
relations: ['newsletterEmail'],
|
||||
})
|
||||
const subscription = await getRepository(Subscription)
|
||||
.createQueryBuilder('subscription')
|
||||
.innerJoinAndSelect('subscription.newsletterEmail', 'newsletterEmail')
|
||||
.where({ user: { id: uid } })
|
||||
.andWhere('LOWER(name) = LOWER(:name)', { name }) // case insensitive
|
||||
.getOne()
|
||||
|
||||
if (!subscription) {
|
||||
return {
|
||||
errorCodes: [UnsubscribeErrorCode.NotFound],
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import axios from 'axios'
|
||||
import { NewsletterEmail } from '../entity/newsletter_email'
|
||||
import { Subscription } from '../entity/subscription'
|
||||
import { getRepository } from '../entity/utils'
|
||||
import { SubscriptionStatus } from '../generated/graphql'
|
||||
import { sendEmail } from '../utils/sendEmail'
|
||||
import axios from 'axios'
|
||||
import { NewsletterEmail } from '../entity/newsletter_email'
|
||||
import { createNewsletterEmail } from './newsletters'
|
||||
|
||||
interface SaveSubscriptionInput {
|
||||
@ -15,14 +15,36 @@ interface SaveSubscriptionInput {
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export const UNSUBSCRIBE_EMAIL_TEXT =
|
||||
'This message was automatically generated by Omnivore.'
|
||||
|
||||
export const parseUnsubscribeMailTo = (unsubscribeMailTo: string) => {
|
||||
const parsed = new URL(`mailto://${unsubscribeMailTo}`)
|
||||
const subject = parsed.searchParams.get('subject') || 'Unsubscribe'
|
||||
const to = unsubscribeMailTo.replace(parsed.search, '')
|
||||
|
||||
// validate email address
|
||||
if (!to || !to.includes('@')) {
|
||||
throw new Error(`Invalid unsubscribe email address: ${unsubscribeMailTo}`)
|
||||
}
|
||||
|
||||
return {
|
||||
to,
|
||||
subject,
|
||||
}
|
||||
}
|
||||
|
||||
const sendUnsubscribeEmail = async (
|
||||
unsubscribeMailTo: string,
|
||||
newsletterEmail: string
|
||||
): Promise<void> => {
|
||||
// get subject from unsubscribe email address if exists
|
||||
const parsed = parseUnsubscribeMailTo(unsubscribeMailTo)
|
||||
|
||||
const sent = await sendEmail({
|
||||
to: unsubscribeMailTo,
|
||||
subject: 'Unsubscribe',
|
||||
text: `This message was automatically generated by Omnivore.`,
|
||||
to: parsed.to,
|
||||
subject: parsed.subject,
|
||||
text: UNSUBSCRIBE_EMAIL_TEXT,
|
||||
from: newsletterEmail,
|
||||
})
|
||||
|
||||
|
||||
@ -198,15 +198,16 @@ export const createTestSubscription = async (
|
||||
user: User,
|
||||
name: string,
|
||||
newsletterEmail?: NewsletterEmail,
|
||||
status = SubscriptionStatus.Active
|
||||
status = SubscriptionStatus.Active,
|
||||
unsubscribeMailTo?: string
|
||||
): Promise<Subscription> => {
|
||||
const subscription = new Subscription()
|
||||
subscription.user = user
|
||||
subscription.name = name
|
||||
newsletterEmail && (subscription.newsletterEmail = newsletterEmail)
|
||||
subscription.status = status
|
||||
|
||||
return getRepository(Subscription).save(subscription)
|
||||
return getRepository(Subscription).save({
|
||||
user,
|
||||
name,
|
||||
newsletterEmail,
|
||||
status,
|
||||
unsubscribeMailTo,
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteTestLabels = async (
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
import { createTestSubscription, createTestUser, deleteTestUser } from '../db'
|
||||
import { graphqlRequest, request } from '../util'
|
||||
import { Subscription } from '../../src/entity/subscription'
|
||||
import { expect } from 'chai'
|
||||
import chai, { expect } from 'chai'
|
||||
import 'mocha'
|
||||
import sinon from 'sinon'
|
||||
import sinonChai from 'sinon-chai'
|
||||
import { NewsletterEmail } from '../../src/entity/newsletter_email'
|
||||
import { Subscription } from '../../src/entity/subscription'
|
||||
import { User } from '../../src/entity/user'
|
||||
import { getRepository } from '../../src/entity/utils'
|
||||
import { NewsletterEmail } from '../../src/entity/newsletter_email'
|
||||
import { SubscriptionStatus } from '../../src/generated/graphql'
|
||||
import { UNSUBSCRIBE_EMAIL_TEXT } from '../../src/services/subscriptions'
|
||||
import * as sendEmail from '../../src/utils/sendEmail'
|
||||
import { createTestSubscription, createTestUser, deleteTestUser } from '../db'
|
||||
import { graphqlRequest, request } from '../util'
|
||||
|
||||
chai.use(sinonChai)
|
||||
|
||||
describe('Subscriptions API', () => {
|
||||
let user: User
|
||||
@ -95,4 +101,71 @@ describe('Subscriptions API', () => {
|
||||
return graphqlRequest(query, invalidAuthToken).expect(500)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Unsubscribe', () => {
|
||||
const query = (name: string) => `
|
||||
mutation {
|
||||
unsubscribe(name: "${name}") {
|
||||
... on UnsubscribeSuccess {
|
||||
subscription {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on UnsubscribeError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
it('unsubscribes', async () => {
|
||||
const name = 'Sub_5'
|
||||
const to = 'unsubscribe@omnivore.app'
|
||||
const subject = 'test'
|
||||
// create test newsletter subscriptions
|
||||
const newsletterEmail = await getRepository(NewsletterEmail).save({
|
||||
user,
|
||||
address: 'test_2@inbox.omnivore.app',
|
||||
confirmationCode: 'test',
|
||||
})
|
||||
const subscription = await createTestSubscription(
|
||||
user,
|
||||
name,
|
||||
newsletterEmail,
|
||||
SubscriptionStatus.Active,
|
||||
`${to}?subject=${subject}`
|
||||
)
|
||||
|
||||
// fake sendEmail function
|
||||
const fake = sinon.replace(
|
||||
sendEmail,
|
||||
'sendEmail',
|
||||
sinon.fake.resolves(true)
|
||||
)
|
||||
|
||||
const res = await graphqlRequest(
|
||||
query(name.toUpperCase()),
|
||||
authToken
|
||||
).expect(200)
|
||||
|
||||
expect(res.body.data.unsubscribe.subscription).to.eql({
|
||||
id: subscription.id,
|
||||
})
|
||||
|
||||
const deletedSubscription = await getRepository(Subscription).findOneBy({
|
||||
id: subscription.id,
|
||||
})
|
||||
expect(deletedSubscription).to.be.null
|
||||
|
||||
// check if the email was sent
|
||||
expect(fake).to.have.been.calledOnceWith({
|
||||
to,
|
||||
subject,
|
||||
text: UNSUBSCRIBE_EMAIL_TEXT,
|
||||
from: newsletterEmail.address,
|
||||
})
|
||||
|
||||
sinon.restore()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user