add reply to email API which replies Okay to the sender

This commit is contained in:
Hongbo Wu
2024-04-04 15:53:25 +08:00
parent 67c5de6512
commit 5cc5c559ac
5 changed files with 196 additions and 94 deletions

View File

@ -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<ReplyToEmailErrorCode>;
};
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<ReminderSuccess>;
ReplyToEmailError: ResolverTypeWrapper<ReplyToEmailError>;
ReplyToEmailErrorCode: ReplyToEmailErrorCode;
ReplyToEmailResult: ResolversTypes['ReplyToEmailError'] | ResolversTypes['ReplyToEmailSuccess'];
ReplyToEmailSuccess: ResolverTypeWrapper<ReplyToEmailSuccess>;
ReportItemInput: ReportItemInput;
ReportItemResult: ResolverTypeWrapper<ReportItemResult>;
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<ContextType = ResolverContext, ParentType extends
optInFeature?: Resolver<ResolversTypes['OptInFeatureResult'], ParentType, ContextType, RequireFields<MutationOptInFeatureArgs, 'input'>>;
recommend?: Resolver<ResolversTypes['RecommendResult'], ParentType, ContextType, RequireFields<MutationRecommendArgs, 'input'>>;
recommendHighlights?: Resolver<ResolversTypes['RecommendHighlightsResult'], ParentType, ContextType, RequireFields<MutationRecommendHighlightsArgs, 'input'>>;
replyToEmail?: Resolver<ResolversTypes['ReplyToEmailResult'], ParentType, ContextType, RequireFields<MutationReplyToEmailArgs, 'recentEmailId' | 'reply'>>;
reportItem?: Resolver<ResolversTypes['ReportItemResult'], ParentType, ContextType, RequireFields<MutationReportItemArgs, 'input'>>;
revokeApiKey?: Resolver<ResolversTypes['RevokeApiKeyResult'], ParentType, ContextType, RequireFields<MutationRevokeApiKeyArgs, 'id'>>;
saveArticleReadingProgress?: Resolver<ResolversTypes['SaveArticleReadingProgressResult'], ParentType, ContextType, RequireFields<MutationSaveArticleReadingProgressArgs, 'input'>>;
@ -6417,6 +6448,20 @@ export type ReminderSuccessResolvers<ContextType = ResolverContext, ParentType e
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ReplyToEmailErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['ReplyToEmailError'] = ResolversParentTypes['ReplyToEmailError']> = {
errorCodes?: Resolver<Array<ResolversTypes['ReplyToEmailErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ReplyToEmailResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['ReplyToEmailResult'] = ResolversParentTypes['ReplyToEmailResult']> = {
__resolveType: TypeResolveFn<'ReplyToEmailError' | 'ReplyToEmailSuccess', ParentType, ContextType>;
};
export type ReplyToEmailSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['ReplyToEmailSuccess'] = ResolversParentTypes['ReplyToEmailSuccess']> = {
success?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ReportItemResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['ReportItemResult'] = ResolversParentTypes['ReportItemResult']> = {
message?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@ -7488,6 +7533,9 @@ export type Resolvers<ContextType = ResolverContext> = {
ReminderError?: ReminderErrorResolvers<ContextType>;
ReminderResult?: ReminderResultResolvers<ContextType>;
ReminderSuccess?: ReminderSuccessResolvers<ContextType>;
ReplyToEmailError?: ReplyToEmailErrorResolvers<ContextType>;
ReplyToEmailResult?: ReplyToEmailResultResolvers<ContextType>;
ReplyToEmailSuccess?: ReplyToEmailSuccessResolvers<ContextType>;
ReportItemResult?: ReportItemResultResolvers<ContextType>;
RevokeApiKeyError?: RevokeApiKeyErrorResolvers<ContextType>;
RevokeApiKeyResult?: RevokeApiKeyResultResolvers<ContextType>;

View File

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

View File

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

View File

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

View File

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