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:
Hongbo Wu
2023-06-06 20:42:41 +08:00
committed by GitHub
4 changed files with 128 additions and 29 deletions

View File

@ -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],

View File

@ -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,
})

View File

@ -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 (

View File

@ -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()
})
})
})