diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 93afb8908..1343a034a 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -1616,6 +1616,7 @@ export type Mutation = { optInFeature: OptInFeatureResult; recommend: RecommendResult; recommendHighlights: RecommendHighlightsResult; + replyToEmail: ReplyToEmailResult; reportItem: ReportItemResult; revokeApiKey: RevokeApiKeyResult; saveArticleReadingProgress: SaveArticleReadingProgressResult; @@ -1836,6 +1837,12 @@ export type MutationRecommendHighlightsArgs = { }; +export type MutationReplyToEmailArgs = { + recentEmailId: Scalars['ID']; + reply: Scalars['String']; +}; + + export type MutationReportItemArgs = { input: ReportItemInput; }; @@ -2430,6 +2437,22 @@ export type ReminderSuccess = { reminder: Reminder; }; +export type ReplyToEmailError = { + __typename?: 'ReplyToEmailError'; + errorCodes: Array; +}; + +export enum ReplyToEmailErrorCode { + Unauthorized = 'UNAUTHORIZED' +} + +export type ReplyToEmailResult = ReplyToEmailError | ReplyToEmailSuccess; + +export type ReplyToEmailSuccess = { + __typename?: 'ReplyToEmailSuccess'; + success: Scalars['Boolean']; +}; + export type ReportItemInput = { itemUrl: Scalars['String']; pageId: Scalars['ID']; @@ -4245,6 +4268,10 @@ export type ResolversTypes = { ReminderErrorCode: ReminderErrorCode; ReminderResult: ResolversTypes['ReminderError'] | ResolversTypes['ReminderSuccess']; ReminderSuccess: ResolverTypeWrapper; + ReplyToEmailError: ResolverTypeWrapper; + ReplyToEmailErrorCode: ReplyToEmailErrorCode; + ReplyToEmailResult: ResolversTypes['ReplyToEmailError'] | ResolversTypes['ReplyToEmailSuccess']; + ReplyToEmailSuccess: ResolverTypeWrapper; ReportItemInput: ReportItemInput; ReportItemResult: ResolverTypeWrapper; ReportType: ReportType; @@ -4763,6 +4790,9 @@ export type ResolversParentTypes = { ReminderError: ReminderError; ReminderResult: ResolversParentTypes['ReminderError'] | ResolversParentTypes['ReminderSuccess']; ReminderSuccess: ReminderSuccess; + ReplyToEmailError: ReplyToEmailError; + ReplyToEmailResult: ResolversParentTypes['ReplyToEmailError'] | ResolversParentTypes['ReplyToEmailSuccess']; + ReplyToEmailSuccess: ReplyToEmailSuccess; ReportItemInput: ReportItemInput; ReportItemResult: ReportItemResult; RevokeApiKeyError: RevokeApiKeyError; @@ -6128,6 +6158,7 @@ export type MutationResolvers>; recommend?: Resolver>; recommendHighlights?: Resolver>; + replyToEmail?: Resolver>; reportItem?: Resolver>; revokeApiKey?: Resolver>; saveArticleReadingProgress?: Resolver>; @@ -6417,6 +6448,20 @@ export type ReminderSuccessResolvers; }; +export type ReplyToEmailErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type ReplyToEmailResultResolvers = { + __resolveType: TypeResolveFn<'ReplyToEmailError' | 'ReplyToEmailSuccess', ParentType, ContextType>; +}; + +export type ReplyToEmailSuccessResolvers = { + success?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type ReportItemResultResolvers = { message?: Resolver; __isTypeOf?: IsTypeOfResolverFn; @@ -7488,6 +7533,9 @@ export type Resolvers = { ReminderError?: ReminderErrorResolvers; ReminderResult?: ReminderResultResolvers; ReminderSuccess?: ReminderSuccessResolvers; + ReplyToEmailError?: ReplyToEmailErrorResolvers; + ReplyToEmailResult?: ReplyToEmailResultResolvers; + ReplyToEmailSuccess?: ReplyToEmailSuccessResolvers; ReportItemResult?: ReportItemResultResolvers; RevokeApiKeyError?: RevokeApiKeyErrorResolvers; RevokeApiKeyResult?: RevokeApiKeyResultResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index ba442c3a7..90bd58980 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -1454,6 +1454,7 @@ type Mutation { optInFeature(input: OptInFeatureInput!): OptInFeatureResult! recommend(input: RecommendInput!): RecommendResult! recommendHighlights(input: RecommendHighlightsInput!): RecommendHighlightsResult! + replyToEmail(recentEmailId: ID!, reply: String!): ReplyToEmailResult! reportItem(input: ReportItemInput!): ReportItemResult! revokeApiKey(id: ID!): RevokeApiKeyResult! saveArticleReadingProgress(input: SaveArticleReadingProgressInput!): SaveArticleReadingProgressResult! @@ -1811,6 +1812,20 @@ type ReminderSuccess { reminder: Reminder! } +type ReplyToEmailError { + errorCodes: [ReplyToEmailErrorCode!]! +} + +enum ReplyToEmailErrorCode { + UNAUTHORIZED +} + +union ReplyToEmailResult = ReplyToEmailError | ReplyToEmailSuccess + +type ReplyToEmailSuccess { + success: Boolean! +} + input ReportItemInput { itemUrl: String! pageId: ID! diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 9a8fcfa58..95bd3e219 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -150,7 +150,11 @@ import { webhookResolver, webhooksResolver, } from './index' -import { markEmailAsItemResolver, recentEmailsResolver } from './recent_emails' +import { + markEmailAsItemResolver, + recentEmailsResolver, + replyToEmailResolver, +} from './recent_emails' import { recentSearchesResolver } from './recent_searches' import { WithDataSourcesContext } from './types' import { updateEmailResolver } from './user' @@ -316,6 +320,7 @@ export const functionResolvers = { emptyTrash: emptyTrashResolver, fetchContent: fetchContentResolver, exportToIntegration: exportToIntegrationResolver, + replyToEmail: replyToEmailResolver, }, Query: { me: getMeUserResolver, @@ -680,4 +685,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('FetchContent'), ...resultResolveTypeResolver('Integration'), ...resultResolveTypeResolver('ExportToIntegration'), + ...resultResolveTypeResolver('ReplyToEmail'), } diff --git a/packages/api/src/resolvers/recent_emails/index.ts b/packages/api/src/resolvers/recent_emails/index.ts index a4b86a929..0241b5424 100644 --- a/packages/api/src/resolvers/recent_emails/index.ts +++ b/packages/api/src/resolvers/recent_emails/index.ts @@ -7,10 +7,14 @@ import { MarkEmailAsItemErrorCode, MarkEmailAsItemSuccess, MutationMarkEmailAsItemArgs, + MutationReplyToEmailArgs, RecentEmailsError, - RecentEmailsErrorCode, RecentEmailsSuccess, + ReplyToEmailError, + ReplyToEmailErrorCode, + ReplyToEmailSuccess, } from '../../generated/graphql' +import { getRepository } from '../../repository' import { updateReceivedEmail } from '../../services/received_emails' import { saveNewsletter } from '../../services/save_newsletter_email' import { authorized } from '../../utils/gql-utils' @@ -20,27 +24,19 @@ import { sendEmail } from '../../utils/sendEmail' export const recentEmailsResolver = authorized< RecentEmailsSuccess, RecentEmailsError ->(async (_, __, { authTrx, log, uid }) => { - try { - const recentEmails = await authTrx((t) => - t.getRepository(ReceivedEmail).find({ - where: { - user: { id: uid }, - }, - order: { createdAt: 'DESC' }, - take: 20, - }) - ) +>(async (_, __, { authTrx, uid }) => { + const recentEmails = await authTrx((t) => + t.getRepository(ReceivedEmail).find({ + where: { + user: { id: uid }, + }, + order: { createdAt: 'DESC' }, + take: 20, + }) + ) - return { - recentEmails, - } - } catch (error) { - log.error('Error getting recent emails', error) - - return { - errorCodes: [RecentEmailsErrorCode.BadRequest], - } + return { + recentEmails, } }) @@ -49,87 +45,109 @@ export const markEmailAsItemResolver = authorized< MarkEmailAsItemError, MutationMarkEmailAsItemArgs >(async (_, { recentEmailId }, { authTrx, uid, log }) => { - try { - const recentEmail = await authTrx((t) => - t.getRepository(ReceivedEmail).findOneBy({ - id: recentEmailId, + const recentEmail = await authTrx((t) => + t.getRepository(ReceivedEmail).findOneBy({ + id: recentEmailId, + user: { id: uid }, + type: 'non-article', + }) + ) + if (!recentEmail) { + log.info('no recent email', recentEmailId) + + return { + errorCodes: [MarkEmailAsItemErrorCode.Unauthorized], + } + } + + const newsletterEmail = await authTrx((t) => + t.getRepository(NewsletterEmail).findOne({ + where: { user: { id: uid }, - type: 'non-article', - }) - ) - if (!recentEmail) { - log.info('no recent email', recentEmailId) - - return { - errorCodes: [MarkEmailAsItemErrorCode.Unauthorized], - } - } - - const newsletterEmail = await authTrx((t) => - t.getRepository(NewsletterEmail).findOne({ - where: { - user: { id: uid }, - address: ILike(recentEmail.to), - }, - relations: ['user'], - }) - ) - if (!newsletterEmail) { - log.info('no newsletter email for', { - id: recentEmail.id, - to: recentEmail.to, - from: recentEmail.from, - }) - - return { - errorCodes: [MarkEmailAsItemErrorCode.NotFound], - } - } - - const success = await saveNewsletter( - { - from: recentEmail.from, - email: recentEmail.to, - title: recentEmail.subject, - content: recentEmail.html, - url: generateUniqueUrl(), - author: parseEmailAddress(recentEmail.from).name, - receivedEmailId: recentEmail.id, + address: ILike(recentEmail.to), }, - newsletterEmail - ) - if (!success) { - log.info('newsletter not created', recentEmail.id) - - return { - errorCodes: [MarkEmailAsItemErrorCode.BadRequest], - } - } - - // update received email type - await updateReceivedEmail(recentEmail.id, 'article', uid) - - const text = `A recent email marked as a library item - by: ${uid} - from: ${recentEmail.from} - subject: ${recentEmail.subject}` - - // email us to let us know that an email failed to parse as an article - await sendEmail({ - to: env.sender.feedback, - subject: 'A recent email marked as a library item', - text, - from: env.sender.message, + relations: ['user'], + }) + ) + if (!newsletterEmail) { + log.info('no newsletter email for', { + id: recentEmail.id, + to: recentEmail.to, + from: recentEmail.from, }) return { - success, + errorCodes: [MarkEmailAsItemErrorCode.NotFound], } - } catch (error) { - log.error('Error marking email as item', error) + } + + const success = await saveNewsletter( + { + from: recentEmail.from, + email: recentEmail.to, + title: recentEmail.subject, + content: recentEmail.html, + url: generateUniqueUrl(), + author: parseEmailAddress(recentEmail.from).name, + receivedEmailId: recentEmail.id, + }, + newsletterEmail + ) + if (!success) { + log.info('newsletter not created', recentEmail.id) return { errorCodes: [MarkEmailAsItemErrorCode.BadRequest], } } + + // update received email type + await updateReceivedEmail(recentEmail.id, 'article', uid) + + const text = `A recent email marked as a library item + by: ${uid} + from: ${recentEmail.from} + subject: ${recentEmail.subject}` + + // email us to let us know that an email failed to parse as an article + await sendEmail({ + to: env.sender.feedback, + subject: 'A recent email marked as a library item', + text, + from: env.sender.message, + }) + + return { + success, + } +}) + +export const replyToEmailResolver = authorized< + ReplyToEmailSuccess, + ReplyToEmailError, + MutationReplyToEmailArgs +>(async (_, { recentEmailId }, { uid, log }) => { + const recentEmail = await getRepository(ReceivedEmail).findOneBy({ + id: recentEmailId, + user: { id: uid }, + }) + + if (!recentEmail) { + log.info('no recent email', recentEmailId) + + return { + errorCodes: [ReplyToEmailErrorCode.Unauthorized], + } + } + + const result = await sendEmail({ + to: recentEmail.from, + subject: 'Re: ' + recentEmail.subject, + text: 'Okay', + from: recentEmail.to, + }) + + return { + success: result, + } }) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 6b0c3cb01..0c64a00e5 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -3066,6 +3066,20 @@ const schema = gql` FAILED_TO_CREATE_TASK } + union ReplyToEmailResult = ReplyToEmailSuccess | ReplyToEmailError + + type ReplyToEmailSuccess { + success: Boolean! + } + + type ReplyToEmailError { + errorCodes: [ReplyToEmailErrorCode!]! + } + + enum ReplyToEmailErrorCode { + UNAUTHORIZED + } + # Mutations type Mutation { googleLogin(input: GoogleLoginInput!): LoginResult! @@ -3165,6 +3179,7 @@ const schema = gql` contentType: String! ): UploadImportFileResult! markEmailAsItem(recentEmailId: ID!): MarkEmailAsItemResult! + replyToEmail(recentEmailId: ID!, reply: String!): ReplyToEmailResult! bulkAction( query: String! action: BulkActionType!