【個人開発向け】お問合せ画面とGmail通知

# はじめに
普段個人開発をしていて、あまりお問合せ画面などを考慮していませんでした。というのも、作って満足していたので、そういうのは後回しにしていました。
今回ちゃんと実装してみようと思い、お問合せ画面を実装してみましたが、firestoreにデータを格納するだけの処理になってしまい・・実際に使ったら通知来ないから意味ないじゃん・・となってしまいました。
けど、個人開発だからSendGridまではいいかな・・と思っていたら、cloud functionsで全て解決したので、記事にしてみました！
多分、規模が大きければ、あまり良くないのかもしれませんが、個人開発ぐらいの規模には最適だと思います！

# 概要
前置きが少し長くなってしまいましたが、今回はcloud functionsを使って、firetoreにお問合せのデータが登録されたら、Gmailにメールが送信されるという自動化を図っていきます！

処理の流れは以下のようになります。
```
1, お問合せ画面から問い合わせ情報が送信
2, firestoreのcontactsコレクションに登録
3, 登録されたことをトリガーにfunctionsで処理
4, Gmailでお問合せ内容が送信される
```

# 画面の実装
ここは、そこまで重要じゃないですが、一応載せておきます。
`Next.js`と`Chakra-ui`、`react-hook-form`を使ってお問合せフォームを実装してます。
```typescript
'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への格納は、以下になります
```typescript
/**
 * お問合せの送信
 * @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](https://nodemailer.com/about/)

結論から言うと、モジュールをインストールしたらすぐにできるわけではありませんでした。
コードをざっと実装しましたが、結果送信されず・・・途方に暮れていると下記の記事を見つけました。
https://dev.classmethod.jp/articles/nodemailer-gmail/

>結論から先に書くと、執筆時現在ではGmailで送る際に
>2段階認証を設定してあること
>アプリで使う「アプリパスワード」を設定し、そのパスワードを使用すること
>が必要なようです。

らしいです。そのため、実装前に、これも設定しておきましょう。
設定ができたら、下記コードをdeployしましょう。
```typescript
/** 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の登録トリガーは下記のコードで実装できるようです。
```typescript
functions
  .region('asia-northeast1')
  .firestore.document('contacts/{contactId}')
  .onCreate
```
これを`users`とかに変えると、ユーザ情報が登録されたときに動作させたりできます。
また、登録だけでなく、`onUpdate`や`onDelete`、`onWrite`など様々なトリガーとなる処理があるようです。
https://firebase.google.com/docs/functions/firestore-events?hl=ja&gen=1st

これで、お問合せが来てもすぐに対応ができて、宛先もわかるので、すぐに返信できちゃいますね。

# 終わりに
今回は、functionsを使って、firestoreの登録トリガーからGmail送信までを実装してみました。少しずつfunctonsを活用できるようになった気がします。
少しでもこの記事がお役に立てると幸いです。また間違い等ございましたら、ご指摘のほどお願い致します。