Gmail APIで安全なメール解除サポートサイトを作ってみる

Gmail APIで安全なメール解除サポートサイトを作ってみる:

昨年末急に「世の中の役に立つWebサービスを作りたい」と思い立ち、

とりま月に1本ずつWebサービスを作ってみようと思ってます。いつまで続くかはわかりません。ペースもおおむねという感じです。

で、先月は、株式関連で適時開示IRをWebSocket接続でGoogle以上に深く?サクサク検索できる「株だネット」という検索ツールを作ってみたのですが、まぁ東証さんに許可が取れなければ閉鎖するかも。(もし東証に権利を持っている方で使いたいという方がいらっしゃればご連絡ください)

さて、今月は目先を変えてメール関連の便利サービスを作ってみることにしました。

これです。
「メール解除サポート」



image.png


では、UIは手抜きですが (^^; Gmail API を使って処理した機能の解説をします。


動機
楽天やYahoo!やAmazonなどを使う時、注意していないといつのまにかメール配信サービスへの登録がどんどん増えていきます。

やがてそれらの大半は開くことも無くなりますが、延々と届き続け、自分のメーラーを埋め尽くし、必要なメールの閲覧を困難にしていきます。

それはインターネットリソースの無駄使いでもあります。

でも、それらのメールをひとつひとつ開いて解除する作業は意外に面倒で億劫です。

そこで、それを抽出して作業を楽にしてくれるサービス「メール解除サポート」を作ってみました。

受信メールから配信解除リンク等を抽出してリストアップし、不要なメールの配信解除を簡単にします。

また、重複する配信先は最新のものひとつだけに限定するので無数のメールから自分で探すよりも楽です。

今回は Gmail API を使うので Gmail 利用者限定。

安全性は下にも書きましたが、サイト側からはメール内容に一切触れる事すらできないようにしているので安全といえるかもしれません。


Gmail API用のOAuthクライアントIDの取得


Gmail API を有効化
Gmailをプログラムから利用するには、まず、自分の Google アカウントでGoogle Developer Consoleへ入り

最初に Gmail API を有効化する必要があります。

「APIとサービスの有効化」で「Gmail API」のちょっと大きめのアイコンを探してクリックし、



image.png


有効化します。



image.png


このAPIを試す」と「チュートリアルとドキュメント」のリンクは有用なのでメモしておきましょう。


認証情報を作成する
次に、認証情報を作成します。



image.png


「認証情報を作成」をクリックすると

次のページが開くので、



image.png


それぞれの項目を上記のように選んで「必要な認証情報」をクリックすると、



image.png


「プロジェクトへの認証情報の追加」が開くので、

  • 「OAuth 2.0 クライアント ID を作成する」でまず「名前」を適当につけます。ここでは webClient-1
  • 「承認済みの JavaScript 生成元」はここでは https://kabuda.net
  • 承認済みのリダイレクト URIはこのケースではHTMLを置くページ https://kabuda.net/kaijyo.html を指定しておきます。OAuth承認後このページへリダイレクトされ、下記スコープで指定したGmail処理などができるようになります。
以上の設定の後、「OAuthクライアントID」ボタンを押します。

※その前にこの kabuda.net ドメインが「[OAuth 同意設定で承認済みドメインのリストに追加」されている必要があるので、まだの場合は上の方のそのリンクをクリックします。



image.png


ここで、「OAuth同意画面」と「ドメインの確認」タブを開いて設定します。 

  • 「アプリケーション名」はサイトやサービスの名前て適当に付けます。
  • 「Google API のスコープ」は、デフォルトの3つだけならそのまま使えます。今回は gmail のread属性が必要なので、
https://www.googleapis.com/auth/gmail.readonly

を選択するのですが「重要なスコープを追加したため、スコープの公開前に、同意画面で Google による確認が必要です。」と表示されてGoogleの承認待ちとなります。

  • 「承認済みドメイン」は「ドメインの確認」タブで登録しGoogleがドメインの所有権を確認したものを書きます(ここでは kabuda.net)
  • [アプリケーション ホームページ] リンクと[アプリケーション プライバシー ポリシー] リンクは必須なのでプライバシー ポリシーページもちゃんと作ります。
そして「OAuthクライアントIDを作成」ボタンを押します。



image.png


するとこんな感じで「OAuth 2.0 クライアント ID」が生成されるので、これを下記のコードに貼りこみます。

これで出来上がりなのですが、Googleの承認が下りないと、

このコードの「サインイン」ボタンを押してリダイレクトページに戻るには、Googleからのこのページは危ないかも?というお知らせに「平気っす」という感じで答えて進む必要があります。


表示結果

サインインして「Get 配信停止リンク」ボタンを押すととりあえずメール100件から「配信停止」などの文字列を探してその前後1000文字程度を下記のようにリストアップしてくれます。

そこにあるリンクが普通は配信停止用のリンクなのでそれをクリックしてそれぞれの解除手続きに従います。

その時、重複する配信先は最新のものひとつだけに限定するので自分で探すよりも楽です。



image.png



コード

では、HTMLを眺めてみます。

kaijyo.html
<script src=./js/jquery-1.12.4.js></script> 
<script src=./node_modules/js-base64/base64.min.js></script> 
 
<button onclick="signIn()">Sign in</button> 
<button onclick="getEmail()">Get 配信停止リンク</button> 
<button onclick="signOut()">Sign out</button> 
 
<!-- Sign in ステータス を出力します --> 
<div id=sign></div> 
<!-- HTML を出力します --> 
<div id=res></div> 
 
<script> 
(function(window){ 
 
  // キーワードリスト この文字列を検索します 
  const wordLists=[ 
    'unsubscribe', 
    '受信解除', 
    'メールの配信を停止', 
    'メール配信を停止', 
    'メール配信停止', 
    'メールの配信停止', 
    '配信を停止', 
    '配信停止', 
    'メールの配信解除', 
    'メール配信解除', 
    '配信解除', 
    '購読を停止', 
    'メールを解除', 
    'メールマガジンを停止', 
    'メールマガジンの停止', 
    'メールマガジン停止', 
    'メールマガジン解除', 
    'このようなEメールを受け取りたくない', 
  ]; 
  
  // OAuth 2.0 クライアント ID 
  const CLIENT_ID = '1011354182387-vusdhpj0u044auq13r1j81aei199vjmb.apps.googleusercontent.com'; 
  let addressLists=[]; 
 
  // https://apis.google.com/js/client.js?onload=onLoad で動作します 
  async function onLoad() { 
    try { 
      await gapi.load('client:auth2'); 
      await gapi.client.init({ 
          clientId: CLIENT_ID, 
          scope:'https://www.googleapis.com/auth/gmail.readonly' 
      }); 
      await gapi.client.load('gmail', 'v1'); 
      chkSignIn(gapi); 
      $('#res').fadeIn(); 
 
    } catch (e) { 
      console.error(e); 
    } 
  } 
 
  // サインインの処理 
  async function signIn() { 
    try { 
      await gapi.auth2.getAuthInstance().signIn(); 
      chkSignIn(gapi); 
      $('#res').fadeIn(); 
    } catch (e) { 
      console.error(e); 
    } 
  } 
 
  // サインアウトの処理 
  async function signOut() { 
    try { 
      await gapi.auth2.getAuthInstance().signOut(); 
      chkSignIn(gapi); 
      $('#res').fadeOut(); 
    } catch (e) { 
      console.error(e); 
    } 
  } 
 
  // メールから上記ワード検索して出力します 
  async function getEmail() { 
 
    let hasAddr=false; 
 
    try { 
 
      // サインイン済みかチェック。 
      let signIn=chkSignIn(gapi); 
      if (!signIn) { 
        return; 
      } 
 
      //メッセージリストの取得 
      let res = await gapi.client.gmail.users.messages.list({ 
          'userId': 'me', 
      }); 
      let cnt=0; 
      if(!res.result){ 
          return; 
      } 
 
      for(let i in res.result.messages){ 
          let newestMessageId = res.result.messages[i].id; 
          let res2 = await gapi.client.gmail.users.messages.get({ 
              userId: 'me', 
              id: newestMessageId 
          }); 
 
          if(res2.result.payload.parts){ 
              let base64Body = res2.result.payload.parts[0].body.data; 
              let body = Base64.decode(base64Body); //bodyはBase64 
 
              let html=''; 
 
              html+='<div class=mailListHeader>' 
              res2.result.payload.headers.forEach(function(item,i){ 
                  if(item.name==='From'){ 
                      let address=item.value.match(/<(.*)>/) 
                      let addrStr=item.value.split(' ').join('') 
                      hasAddr=chkAddr(addressLists, addrStr); 
                      if(!hasAddr)addressLists.push(addrStr) 
 
                      if(address ){ 
                          html+=item.name+':'+'<b><span style=color:#fff>'+item.value+'</span></b><'+ address[1]+'><br>'; 
                      } else { 
                        html+=item.name+':'+'<b><span style=color:#fff>'+item.value+'</span></b><br>'; 
                      } 
 
                  } 
                  if(item.name==='Subject'){ 
                      html+=item.name+':'+item.value+'<br>'; 
                  } 
                  if(item.name==='Date'){ 
                      html+=item.name+':'+item.value+'<br>'; 
                  } 
 
              }) 
              html+='</div>' 
 
              // 文字列抽出 
              let str='' 
              str+=mkStr(wordLists, body) 
 
              // 出力 
              if(!hasAddr){ 
                $('#res').fadeIn(); 
                $('#res').append( 
                  html 
                  +'<div class=lists>' 
                  +'<div class=kaizyoTitle>配信解除抽出</div>' 
                  +((str!=='')?str:'「配信解除」「配信停止」といった文字列がみあたりませんでした') 
                  +'</div>' 
                ); 
              } 
 
              cnt++ 
 
              if(cnt>100){ 
                  break; 
              } 
          } 
      } 
 
    } catch (e) { 
      console.error(e); 
    } 
  } 
 
  // サインイン済みかチェック。 
  function chkSignIn(gapi){ 
      if (!gapi.auth2.getAuthInstance().isSignedIn.get()) { 
        console.error('First, prease Sign in'); 
        document.getElementById('sign').innerHTML='Now Sign out. First, prease Sign in' 
        return false; 
      } else { 
        document.getElementById('sign').innerHTML='Now Sign in.' 
        return true; 
      } 
  } 
 
  // 抽出した文字列 
  function mkStr(wordLists, body){ 
    let str=''; 
    for(let i=0;i<wordLists.length;i++){ 
      str+=findWords(wordLists[i], body) 
    } 
    return str; 
  } 
 
  // 重複アドレスの判定 
  function chkAddr(addressLists,  value){ 
    if(addressLists.indexOf(value)!==-1){ 
      return true; 
    } else { 
      return false; 
    } 
  } 
 
  // bodyにWordが含まれているか調べる 
  function chkWords(word, body){ 
      if(body.indexOf(word)!==-1){ return true;} 
      return false; 
  } 
 
  // bodyからWordを探しその前後の文字列を抽出する 
  function findWords(word, body){ 
      if(body.indexOf(word)!==-1){  
        let ary=body.split(word); 
        let pre=ary[0]; 
        let next=ary[1]; 
        pre=pre.substr(-200, 200); 
        next=next.substr(0, 800); 
        // urlにリンクを付ける 
        next=text2url(next) 
        let str='...'+pre+'<span style=color:red>'+word+'</span>'+next+'...' 
        return str 
      } else { 
        return '' ; 
      } 
 
  } 
 
  // text内のurlにAタグをつける 
  function text2url(str){ 
    let text = str.replace(/(http(s)?:\/\/[a-zA-Z0-9-.!'()*;/?:@&=+$,%#_]+)/gi, "<a href='$1' target='_blank'>$1</a>"); 
    return text; 
  } 
  window.onLoad=onLoad; 
  window.signIn=signIn; 
  window.signOut=signOut; 
  window.getEmail=getEmail; 
 
})(window); 
</script> 
 
<!-- Google APIs Client Libraryの読み込み。読み込み後にonloadに指定した関数が呼ばれる。 --> 
<script src="https://apis.google.com/js/client.js?onload=onLoad"></script> 


安全性

この辺はもしかすると異論があるかもしれないので気になることがあればコメントへお願いします。因みに読み込んでいる3つの外部ライブラリの安全性は議論の外になります。

  • この「メール解除サポート」ではサイト側は、顧客情報もメールデータも取得しません
ページの中で読み込んでいるのは jquery と base64 と apis.google.com/js/client.js の三つだけです。

このサイト ( kabuda.net ) 自身のサーバーやDBなどへの接続はありません。

つまり、Google API で取得されたメールデータを kabuda.net 側が見ることは無いということです。

jquery と base64 は著名なライブラリであり、client.js は Google API で

OAuth 2.0 による認証を実現し Gamil を操作するためのものです。

そして、

  • このコードは、ユーザーが開いているブラウザ内の他人からはアクセス不能なブロックスコープ内で処理されます
当サイトのDBなどには一切接続しません。

一応参考のために書いておくと2018-01-06現在のこのファイルから sha256sum で生成した SHA256 ハッシュ値は、

$ sha256sum  ./kaijyo.html 
d2bccec8906df4e9223a79c84db5cf23eca85030c588c0f1e96f087060faf26e 
となります。


参考にしたサイト

有難うございます

コメント

このブログの人気の投稿

投稿時間:2021-06-17 05:05:34 RSSフィード2021-06-17 05:00 分まとめ(1274件)

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2020-12-01 09:41:49 RSSフィード2020-12-01 09:00 分まとめ(69件)