cloud functionsでGmailを送信する
はじめに
開発中のアプリにお問い合わせ画面をいれてお問合せがあった場合、自動でメールが送信されるように実装してみました。その時に、ちょっと手こずったので、備忘録を残しておきます。
あと、結構いい感じの自動化で、いろんな場面で使えそうだったので、誰かのお役に立てると幸いです。
フロー
処理の流れは以下のようになります。
1, お問合せ画面から問い合わせ情報が送信
2, firestoreのcontactsコレクションに登録
3, 登録されたことをトリガーにfunctionsで処理
4, Gmailでお問合せ内容が送信される
画面の実装
ここは、そこまで重要じゃないですが、一応載せておきます。
Next.js
とChakra-ui
、react-hook-form
を使ってお問合せフォームを実装してます。
'use client'
import { useRouter } from 'next/navigation'
import { useForm } from 'react-hook-form'
import {
Box,
Button,
Flex,
FormLabel,
Heading,
Input,
Textarea,
useToast,
VStack,
} from '@/common/design'
import { validateContactScreen } from '@/common/utils/validate'
import { postContact } from '@/lib/firebase/api/contacts'
/** お問い合わせ送信画面
* @screenname PostContactScreen
* @description お問い合わせを送信する画面
*/
export default function PostContactScreen() {
const router = useRouter()
const toast = useToast()
const { handleSubmit, register } = useForm()
const onSubmit = handleSubmit(async (data) => {
const result = validateContactScreen({
title: data.title,
content: data.content,
})
if (result.isSuccess) {
await postContact({ title: data.title, content: data.content }).then(
(res) => {
if (res.isSuccess) {
toast({
title: res.message,
description: '貴重なご意見ありがとうございます',
status: 'success',
duration: 2000,
isClosable: true,
})
router.back()
} else {
toast({
title: res.message,
status: 'error',
duration: 2000,
isClosable: true,
})
}
}
)
} else {
// バリデーションエラー
toast({
title: result.message,
status: 'error',
duration: 2000,
isClosable: true,
})
}
})
return (
<Flex flexDirection='column' width='100%' gap='24px' paddingY='5px'>
<Heading fontSize='22px'>お問い合わせフォーム</Heading>
<form onSubmit={onSubmit}>
<VStack alignItems='left'>
<Box>
<FormLabel htmlFor='title' textAlign='start'>
タイトル
</FormLabel>
<Input id='title' {...register('title')} />
</Box>
<Box>
<FormLabel htmlFor='content' textAlign='start'>
お問い合わせ内容
</FormLabel>
<Textarea id='content' {...register('content')} />
</Box>
<Button
marginTop='4'
colorScheme='teal'
type='submit'
paddingX='auto'
>
送信
</Button>
<Button marginTop='4' paddingX='auto' onClick={() => router.back()}>
戻る
</Button>
</VStack>
</form>
</Flex>
)
}
firestoreへの格納は、以下になります
/**
* お問合せの送信
* @param title
* @param content
* @returns Promise<FirebaseResult>
*/
export const postContact = async (args: {
title: string
content: string
}): Promise<FirebaseResult> => {
let result: FirebaseResult = {
isSuccess: false,
message: '',
}
const user = auth.currentUser
const colRef = collection(db, 'contacts')
const userInfo = await getUserInfoByUid({ uid: user!.uid })
if (userInfo) {
await addDoc(colRef, {
contactTitle: args.title,
contactContent: args.content,
createdAt: serverTimestamp(),
contactFrom: {
uid: userInfo.uid,
username: userInfo.username,
email: userInfo.email,
},
}).then(() => {
result = {
isSuccess: true,
message: 'お問い合わせを送信しました。',
}
})
} else {
result = {
isSuccess: false,
message: 'ユーザ情報の取得に失敗しました。',
}
}
return result
}
その後のお問合せ対応にも使えそうなので、お問合せを送信してくれた方の情報も登録してます。
では、cloud functionsを使用して、contacts
コレクションに登録されたことをトリガーにGmailが送信されるように実装していきます。
functionsの実装
functionsはtypescript
を使って実装していきます。
Gmailを送信するために、「Nodemailer」というモジュールを入れていきます。
Nodemailer
結論から言うと、モジュールをインストールしたらすぐにできるわけではありませんでした。
コードをざっと実装しましたが、結果送信されず・・・途方に暮れていると下記の記事を見つけました。
結論から先に書くと、執筆時現在ではGmailで送る際に
2段階認証を設定してあること
アプリで使う「アプリパスワード」を設定し、そのパスワードを使用すること
が必要なようです。
らしいです。そのため、実装前に、これも設定しておきましょう。
設定ができたら、下記コードをdeployしましょう。
/** Gmail送信用の設定変数 */
let transporter = nodemailer.createTransport({
service: 'Gmail',
auth: {
user: {設定したemail},
pass: {設定したアプリパスワード},
},
})
/** firestoreのcontactsに新規作成されたときに動作する処理 */
const createContactTriggerFromFirestore = functions
.region('asia-northeast1')
.firestore.document('contacts/{contactId}')
.onCreate(async (snapshot, context) => {
const timestamp = snapshot.data().createdAt.seconds
const date = new Date(parseInt(timestamp, 10) * 1000)
const contact: Contact = {
contactId: snapshot.id,
contactTitle: snapshot.data().contactTitle,
contactContent: snapshot.data().contactContent,
contactFrom: snapshot.data().contactFrom,
createdAt: date,
}
let message = 'このメールは自動送信です。\n'
message += '下記の内容のお問い合わせがありました。\n\n'
message += '~~~~~~~~~~~~~~~~~~~~~~~~~~\n'
message += `タイトル : ${contact.contactTitle}\n`
message += `ユーザ名 : ${contact.contactFrom.username}\n`
message += `日時 : ${format(
contact.createdAt,
'yyyy年MM月dd日'
)}\n`
message += `問い合わせ内容 :\n ${contact.contactContent}\n`
message += '~~~~~~~~~~~~~~~~~~~~~~~~~~\n'
try {
const mailOptions = {
from: {送信先のGmail},
to: {送信元のGmail},
subject: {お問合せのタイトル},
text: message,
}
await transporter.sendMail(mailOptions, (error, info) => {
if (error) {
return logColRef.add({
status: 'error',
message: error,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
})
}
return logColRef.add({
status: 'success',
message: 'メールが正常に送信されました',
createdAt: admin.firestore.FieldValue.serverTimestamp(),
})
})
} catch (error) {
logColRef.add({
status: 'error',
message: error,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
})
return
}
})
firestoreの登録トリガーは下記のコードで実装できるようです。
functions
.region('asia-northeast1')
.firestore.document('contacts/{contactId}')
.onCreate
これをusers
とかに変えると、ユーザ情報が登録されたときに動作させたりできます。
また、登録だけでなく、onUpdate
やonDelete
、onWrite
など様々なトリガーとなる処理があるようです。
これで、お問合せが来てもすぐに対応ができて、宛先もわかるので、すぐに返信できちゃいますね。
終わりに
今回は、functionsを使って、firestoreの登録トリガーからGmail送信までを実装してみました。少しずつfunctonsを活用できるようになった気がします。
少しでもこの記事がお役に立てると幸いです。また間違い等ございましたら、ご指摘のほどお願い致します。
Discussion