またどこかのCTOになった人のブログ

またどこかのCTOになった人が書いてます。最近はCakePHP3とConoHaのネタが多いです。

fullcalendarを使ってみた記事をまとめてみた。

あっさり書くつもりだったfullcalendarの記事が、うっかり5記事になってしまったのでまとめてみました。
でも、Cake3だったらfullcalendarのプラグインがあったしますのでそれを使ってもいいかもしれません。

github.com

自分は使ったことがないのでよくわかりません。ごめんなさい。


以下はfullcalendarの記事です。
全部ではないかもしれませんが、書いている内容はCake3に依存してはいないと思いますのでfullcalendarを使う機会がありましたら参考にして下さい。

tsuralabo.hatenablog.com
tsuralabo.hatenablog.com
tsuralabo.hatenablog.com
tsuralabo.hatenablog.com
tsuralabo.hatenablog.com

fullcalendar その5 〜イベントをドラッグしてイベントの日付を変更してみた〜

まさかの5回目です。
過去の記事は以下をご参照下さい。

tsuralabo.hatenablog.com
tsuralabo.hatenablog.com
tsuralabo.hatenablog.com
tsuralabo.hatenablog.com


・fullcalendarの外枠を作ってプロパティ設定できた
・表示している月だけのカレンダーデータを取得できた
・設定済みのイベントをクリックしてモーダル表示、編集とかも何となくできた
・カレンダーをクリックしたりドラッグしたりしてイベント追加できた

多分できています。
仕上げは設定済みのイベントをドラッグして別の日に移動させてデータ更新です。

fullcalendarのプロパティを確認

「editable」をtrueに設定する必要があります。
これをやっておかないと一生動きません。
多分設定済みだとは思うのですが、一応確認しておいて下さい。

イベントドラッグイベントを登録

使用するのは「eventDrop」というイベントです。
ドラッグ完了時みたいな感じのイメージです。
サラっと書いてみます。

eventDrop: function(event, delta, revertFunc, jsEvent, ui, view) {
    // ***** ここにドラッグ完了時の処理を書く *****
}

引数の意味合いはグーグル先生にお聞き下さい。
自分はeventしか今回使わなかったのでよくわかりません。

既にあるイベントデータを更新するわけですから、以下のデータがあれば実現できそうです。

・更新対象のイベントのID(今回の例ですと、schedules.idになります)
・日付移動させた先の開始日
・日付移動させた先の終了日

試行錯誤した結果、こんな感じにしました。

eventDrop: function(event, delta, revertFunc, jsEvent, ui, view) {
    // ドラッグ後の日付にデータ更新する
    moveSchedule(event.id, event.start.format('YYYY-MM-DD'), event.end.format('YYYY-MM-DD'));
}

eventの中から必要なデータを取得して、引数としてJSに渡します。
渡した先のJS内でAjax使ってsaveすれば変更完了です。

save部分は普通のCake3の処理と変わりません。
これでなんとなくfullcalendarを使ったスケジュール表ができるんじゃないでしょうか。

こんなに記事が長くなると思ってませんでしたが、以上がCakePHP3 + Bootstrap3 + fullcalendarで作るスケジュール表っぽいもののサンプルになります。

問題なくできていますように。

fullcalendar その4 〜カレンダーをクリックしたりドラッグしたりしてイベント追加してみた〜

4回目になってしまいました。
過去の3回分はこちらになります。

tsuralabo.hatenablog.com
tsuralabo.hatenablog.com
tsuralabo.hatenablog.com

クリックとドラッグの違い

なんて書くとバカっぽいのですが、fullcalendarはクリックかドラッグかをちゃんと判定してくれます。
1回目の記事でちょっと書きましたが、「selectMinDistance」というプロパティがその判定基準です。
ここに設定した値よりクリック時間が長い場合、ドラッグと判定してる「ぽい」です。
selectMinDistanceのデフォルト値は「0」になっているため、1以上の値にしないとクリックとドラッグの判定はできません。
逆に言えば、ここさえ設定しておけば大したことはありません。

とりあえずクリックイベントを追加してみる

クリックはdayClickというイベントを使用します。
回数を追うごとに雑になってきましたが、こんな感じです。

dayClick: function(date, jsEvent, view) {
    // ***** ここに処理を書く *****
},

カレンダーに対してイベントを追加するので、CalendarControllerのaddアクションでも作って飛ばせばいいんじゃないかと。
ということにした場合は、こんな風になります。

dayClick: function(date, jsEvent, view) {
    // ***** 対象日にイベント追加 *****
    location.href = "<?php echo $this->Url->build('/'); ?>calendars/add/" + date.format('YYYY-MM-DD') + "/";
},

addアクションに対象日付を渡しています。
あとはその渡した日に対してデータ登録すればOKじゃないかと思われます。

ドラッグイベントも追加してみる

ドラッグの時はselectイベントを使います。
多分合ってると思うんですけど。

select: function(start, end) {
    // ***** 対象日の範囲にイベント追加 *****
    location.href = "<?php echo $this->Url->build('/'); ?>calendars/add/" + start.format('YYYY-MM-DD') + "/" + end.format('YYYY-MM-DD') + "/" + start.format('HH:mm') + "/" + end.format('HH:mm') + "/";
},


ダサい引数ですね。でも、気にしないことにします。
範囲指定をするとstartとendにドラッグした範囲の年月日開始と終了が入ってきます。
あとは用途に応じてフォーマットをかけて渡してデータ登録処理をすればOKです。
例えば、2017-11-02から2017-11-05までドラッグした場合は、startに「2017-11-02」、endに「2017-11-05」が入ってくるわけです。
上で時間もフォーマットかけて渡しているのは、fullcalendarというものは日付だけではなく時間も設定するとカレンダー上のイベントの左側に時間が表示されるのです。
そこをできればnullにしたくなかっただけなのです。
好みみたいなものですので、その辺は柔らかく対応すればいいかなと思います。

fullcalendarはstartとendに合致するデータを範囲表示するように、どこかの偉い人が作ってくれておりますのでテーブルのstart、endに相当するカラムにセットすれば恐らくそのまま帯表示されるはずです。

と思うんですが、いかがでしょうか。

fullcalendar その3 〜設定済みのイベントをクリックしてモーダル表示してみた〜

想定より長くなりましたが3回目です。
過去の2回はこちらをご覧下さい。

tsuralabo.hatenablog.com
tsuralabo.hatenablog.com

モーダル表示部分を追加してみる

前々回でBootstrapを使った外枠を作りましたので、そこにモーダル表示部分を追加してみます。
端折ってコピペしたものが以下のコードです。

<div id="contents" class="container-fluid">
  <div class="row">
    <div class="col-xs-12">
      <div class="panel">
        <div class="panel-heading">
          スケジュール表
        </div>
        <div class="panel-body">
          <div class="row">
            <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"
              <div id="calendar"></div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
<!-- ここからモーダル表示部分として追記 -->
<div id="calendarModal" class="modal fade">
  <div class="modal-dialog modal-dialog-center">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal">
          <span aria-hidden="true">×</span>
          <span class="sr-only">close</span>
        </button>
        <span id="modalTitle" class="modal-title"></span>
      </div>
      <div id="modalBody" class="modal-body"></div>
      <div id="modalFooter" class="modal-footer"></div>
    </div>
  </div>
</div>
<!-- ここまでモーダル追加部分で追記しました -->
<script>
  <!-- fullcalendar発火部分 -->
</script>

特に何も凝っておらず普通のモーダルです。

fullcalendarのイベントにモーダル発火部分を書いてみる

さて、とりあえずモーダルの準備ができたので発火させる必要があります。
表示しているイベントをクリックした時に発火させることにします。
アレですね。カレンダーに表示された帯みたいなイベント。
デフォルトだと紺色っぽいアレです。

記載箇所は以下です。
これもまた端折って前回のものをコピペすることにします。

<script>
  $(document).ready(function() {
    $('#calendar').fullCalendar({
        header: {
          left: 'prev,next today',
          center: 'title',
          right: 'month,agendaWeek,agendaDay,listMonth'
        },
        timeFormat: 'HH:mm',
        timezone: 'Asia/Tokyo', 
        eventLimit: true, 
        editable: true, 
        slotEventOverlap: true, 
        selectable: true, 
        selectHelper: true, 
        selectMinDistance: 1, 
        events: function(start, end, timezone, callback) {
           // 前回実装したカレンダーデータ取得部分
           setCalendarList(start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD'), callback);
        },
        eventClick: function(calEvent, jsEvent, view) {
           // ***** 今回はここにクリックイベントを追加 *****
           $('#modalTitle').html(calEvent.title); // モーダルのタイトルをセット
           $('#modalBody').html(calEvent.description); // モーダルの本文をセット
           $('#calendarModal').modal(); // モーダル着火
        },
        dayClick: function(date, jsEvent, view) {
           // カレンダー空白部分クリック時のイベント
        },
        select: function(start, end) {
           // カレンダー空白部分をドラッグして範囲指定した時のイベント
        },
        eventDrop: function(event, delta, revertFunc, jsEvent, ui, view) {
           // イベントをドラッグして別日に移動させた時のイベント
        }
    });
  });
</script>


「eventClick」という名が指す通り、イベントクリック時のイベントはここに書きます。
calEventの中に「クリックされたイベント情報」が詰まっていますので、その中からデータを抜いてモーダルにセットしています。

例えばこれ以外にも編集ボタンとか削除ボタンを実装したいという場合は、今回記載したイベントの中でHTML要素を生成してモーダルにセットすれば大丈夫です。
こんな感じになります。

eventClick: function(calEvent, jsEvent, view) {
   // 編集ボタンと削除ボタン、ついでに閉じるボタンのHTML要素を追加
   var editHtml = 
       '<a role="button" class="btn btn-danger pull-left" href="' + "<?php echo $this->Url->build('/'); ?>" + 'calendars/delete/' + calEvent.id + '/">削除</a>' + 
       '<a role="button" class="btn btn-primary" href="' + "<?php echo $this->Url->build('/'); ?>" + 'calendars/edit/' + calEvent.id + '/">編集</a>' +
       '<a role="button" class="btn btn-default" data-dismiss="modal">閉じる</a>';
   $('#modalTitle').html(calEvent.title);
   $('#modalBody').html(calEvent.description);
   $('#calendarModal').modal(); // モーダル着火
}

CalendarsControllerのdeleteアクションで削除、editアクションで編集ということにしています。
calEventから対象のスケジュールデータのIDを渡してあげれば削除も編集もできると思います(ここは割愛します)

ちょっとずつですが、fullcalendarがカタチになってきているような気がします。
気のせいである可能性はゼロじゃありませんが、まぁいいです。

fullcalendar その2 〜表示している月のデータだけ取得してイベント表示してみた〜

前回の続きになります。
前回の記事はこちらです。

tsuralabo.hatenablog.com

カレンダーデータ格納用テーブルを準備

カレンダーデータなので、きっとDBの中に格納されていることでしょう。
ということにして、テーブルはこんな感じで作ります。

CREATE TABLE `schedules` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) DEFAULT NULL COMMENT '予定タイトル',
  `description` varchar(255) DEFAULT NULL COMMENT '予定詳細',
  `calendar_color` varchar(255) DEFAULT NULL COMMENT 'カレンダー背景色',
  `calendar_text_color` varchar(255) DEFAULT NULL COMMENT 'カレンダー文字色',
  `start_date` date DEFAULT NULL COMMENT '開始日',
  `start_time` char(5) DEFAULT NULL COMMENT '開始時刻',
  `end_date` date DEFAULT NULL COMMENT '終了日',
  `end_time` char(5) DEFAULT NULL COMMENT '終了時刻',
  `is_deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '削除フラグ(0:有効 / 1:削除済み)',
  `created` datetime DEFAULT NULL COMMENT 'データ作成日',
  `created_user_id` int(11) DEFAULT NULL COMMENT 'データ作成者',
  `modified` datetime DEFAULT NULL COMMENT 'データ更新日',
  `modified_user_id` int(11) DEFAULT NULL COMMENT 'データ更新者',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='スケジュール管理テーブル';

他に必要なデータがあればカラム追加なり、いらなければ削除なり。
ただ、fullcalendarを使うのであれば日付と時間はカラム単位で分けておいた方が無難です。

カレンダーデータを取得

Ajax実行用のコントローラを作成して、その中にfunctionを作りました。
あとはそのfunctionに対してパラメータを渡してデータ取得です。
取得したデータは配列に格納し直して、JSON形式で返します。

サンプルなのでこんなfunctionにします。

<?php
  /**
   * 日付範囲に合致するスケジュール一覧を取得
   * @param type $startDate
   * @param type $endDate
   */
  public function getScheduleList($startDate, $endDate) {
    
    // スケジュールデータを取得
    $scheduleModel = TableRegistry::get('Schedules');
    $scheduleData = $scheduleModel->getScheduleList($startDate, $endDate);
    
    // 取得データを整形して配列に再セット
    $scheduleList = [];
    
    foreach ($scheduleData as $schedule) {
      $scheduleList[] = [
          'id' => $schedule->id,
          'title' => $schedule->title,
          'description' => $schedule->description,
          'start' => $schedule->start_date.(!empty($schedule->start_time) ? " ".$schedule->start_time : " 00:00"),
          'end' => $schedule->end_date.(!empty($schedule->end_time) ? " ".$schedule->end_time : " 23:59"),
          'color' => $schedule->calendar_color,
          'textColor' => $schedule->calendar_text_color
      ];
    }
    
    // 処理結果をJSON形式で返す
    $this->response->body(json_encode($scheduleList));
  }

「$scheduleList」の中でデータをセットしていますが、idとかtitleとかそういうのは全部fullcalendarがカレンダー表示時に使うプロパティです。
JSでごにょごにょやるのが得意ではないので、データを返す前に整形しています。


別件なのですが。
Ajax実行用のコントローラ内にfunctionを作って、確実にJS側から呼んでいるはずなのにエラーになる場合は多分CSRF制約に引っかかっています。
上手いやり方があるのかもしれませんが、自分は知らないのでAjax実行する時は対象のfunctionをbeforeFilterで解除するようにしています。
あまり合っている自信ないですが、こんな感じです。

<?php
class ApisController extends AppController {
  
  // CSRF解除アクション
  private $csrfAllowAction = [
    'getScheduleList'
  ];
  
  public function beforeFilter(Event $event) {
    parent::beforeFilter($event);
    
    // Ajax送信時のCSRFを無効
    if (in_array($this->request->action, $this->csrfAllowAction)) {
      $this->eventManager()->off($this->Csrf);
    }
    
    $this->viewBuilder()->layout('ajax');
    $this->autoRender = false;
  }

  /* 以下略 */

Ajaxで取得したデータをfullcalendarのコールバックに設定

ここまで書いてアレなんですが、Ajax実行する箇所を書いてなかったです。
あまり気にせず、まとめて書くことにします。

/**
 * 対象日付スケジュールデータセット処理
 * @param {type} startDate
 * @param {type} endDate
 * @param {type} callback
 * @returns {undefined}
 */
function setCalendarList(startDate, endDate, callback) {
  
  $.ajax({
    type: 'post',
    dataType : "text",
    async: true,
    cache: false,
    url : /* ここに getScheduleList を実行するURLを記載 */
  })
  .then(
    function(data) {
      // JSONパース
      var obj = jQuery.parseJSON(data);
      var events = [];
      
      $.each(obj, function(index, value) {
        events.push({
          // イベント情報をセット
          id: value['id'],
          title: value['title'],
          description: value['description'],
          start: value['start'],
          end: value['end'],
          color: value['color'],
          textColor: value['textColor']
        });  
      });
      
      // コールバック設定
      callback(events);
    }
  );

  return;
}


Ajax側で取ったデータを配列ループさせて、event.pushで登録しています。
最後にcallback設定するのを忘れないようにして下さい。
colorとtextColorはそのままなんですが、カレンダー表示時の背景色と文字色です。
別に設定する必要もないのですが、変更もできますよという例で書いてあります。
スケジュール設定画面にカラーピッカーでも埋め込んで、16進数でデータ登録すればいいと思います。

できたっぽいので呼んでみる

前回作成したはずのView側から、今回作成したJSのfunctionを呼んでAjaxでデータ取得してみます。

<script>
  $(document).ready(function() {
    $('#calendar').fullCalendar({
        header: {
          left: 'prev,next today',
          center: 'title',
          right: 'month,agendaWeek,agendaDay,listMonth'
        },
        timeFormat: 'HH:mm',
        timezone: 'Asia/Tokyo', 
        eventLimit: true, 
        editable: true, 
        slotEventOverlap: true, 
        selectable: true, 
        selectHelper: true, 
        selectMinDistance: 1, 
        events: function(start, end, timezone, callback) {
           // ***** ここでカレンダーデータ取得JSを呼ぶ *****
           setCalendarList(start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD'), callback);
        },
        eventClick: function(calEvent, jsEvent, view) {
           // カレンダーに設定したイベントクリック時のイベント
        },
        dayClick: function(date, jsEvent, view) {
           // カレンダー空白部分クリック時のイベント
        },
        select: function(start, end) {
           // カレンダー空白部分をドラッグして範囲指定した時のイベント
        },
        eventDrop: function(event, delta, revertFunc, jsEvent, ui, view) {
           // イベントをドラッグして別日に移動させた時のイベント
        }
    });
  });
</script>


こんな感じで呼びます。
startとendには「表示中のカレンダーに該当する月の月初日と月末日」が入っています。
ですので、例えば表示している月が2017年11月であればstartには「2017年11月01日」、endには「2017年11月30日」に相当する値が入っています。
それをフォーマットして渡しています(フォーマット形式はテーブル構成で変わるかもしれません)
callbackはそのまま渡してOKです。

eventsに書いた処理は年月等を変更した時に呼ばれます。
前月とか翌月とか。
上の例で言うとheaderに書いた辺りです。

よくわからなかったらconsole.logを仕込むとかalertを仕込んだりして動かしてみるといいかと思います。

fullcalendar その1 〜とりあえずスケジュール表の外枠だけ作ってみた〜

転職してから4ヶ月が経過しました。
孤独なCTO活動もなかなか慣れてまいりました。
慣れただけであって、平気だというわけではないんですが。


さて。
今回はfullcalendarを使ってスケジュール表を作ってみました。
fullcalendarというものは、どこかの偉い人が作った高機能カレンダーJSライブラリです。

fullcalendar.io

スケジュール表ではないのですが、前職でfullcalendarを使って某機能を作成したことがありました。
が、その時は失敗しました。

datatablesもそうなのですが、JSライブラリなので何も考えないで使うと表示しようとしたデータを最初にまるっと読み込むのです。
なので、データ量が多いと当然動作が重くなったり画面が真っ白になったりします。というかしました。
何も考えないで作ってはいけないということを学んだのです(戒め)

やりたかったこと

・表示している月毎のデータだけを取得したい
・設定済みの予約をクリックしてモーダル表示で詳細を見たい
・設定済みの予約をカレンダー上でドラッグして日付変更したい
・カレンダーの日付をクリックして新規予定を設定したい

編集だとか削除だとか、そういうのはとりあえず置いておきます。

fullcalendarの準備

上にリンクを置いておきましたが、とりあえずfullcalendarをダウンロードして解凍して下さい。
解凍して出てきたディレクトリから、今回使用するのは以下のファイルです。

・JS本体として「fullcalendar.min.js」
・日本語対応として「ja.js」
・fullcalendarの見た目用に「fullcalendar.min.css

この辺を読み込んで下さい。
JQueryは3.1.1を使っています。
fullcalendar.min.jsはJQueryの後の方がいいんじゃないかなと思います、多分。

コントローラ側の記載

何も書きませんでした。

いわゆる管理画面的なものに今回のスケジュール表を載せましたので、CalendarsControllerみたいなものににindexアクションを作った程度です。
データ取得とかそういうのは全部Ajaxでやりました。
まぁAjax嫌いなんですけどね。

View側の記載

カレンダー表示はこんな感じで書きました。
例の如く、今回もBootstrapさんが大活躍です。

<div id="contents" class="container-fluid">
  <div class="row">
    <div class="col-xs-12">
      <div class="panel">
        <div class="panel-heading">
          スケジュール表
        </div>
        <div class="panel-body">
          <!-- メッセージ表示したい時はこの辺に -->
          <div class="row">
            <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"
              <div id="calendar"></div> <!-- ここがカレンダー表示部分 -->
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

パネルが好きなのです。
Bootstrap4はパネルなくなってるらしいですが、使ってるのはBootstrap3なのでいいです。
「パネルの中にカレンダーを表示」させるつもりで書いてます。

fullcalendarを着火

着火はJSで実行します。
カレンダーを表示するViewのどこかにJSを書けばOKです。

<script>
  $(document).ready(function() {
    $('#calendar').fullCalendar();
  });
</script>

これでカレンダーが表示されるんじゃないかなと思われます。

fullcalendarのプロパティ設定

上記のコードでカレンダーは表示されるのですが、色々と都合がよろしくないのでプロパティを追加することにします。
プロパティに関しては以下のページを参考にして下さい。

Documentation | FullCalendar

あまりに雑だし英語ばかりなので、サンプルとして今回自分が設定したプロパティを書いておきます。
参考にして下さい。

<script>
  $(document).ready(function() {
    $('#calendar').fullCalendar({
        header: {
          // fullcalendarのヘッダーに配置するボタン
          // 左側には「前月、翌月、今日」のボタン
          // 中央には表示している月
          // 右側には月表示、週表示、日表示、月予定
          left: 'prev,next today',
          center: 'title',
          right: 'month,agendaWeek,agendaDay,listMonth'
        },
        timeFormat: 'HH:mm', // 時間表示フォーマット
        timezone: 'Asia/Tokyo', // タイムゾーン設定
        eventLimit: true, // イベント増えた時にリンクボタン表示
        editable: true, // 編集可能設定
        slotEventOverlap: true, // イベントの見た目を重ねて表示
        selectable: true, // カレンダー空白部分のドラッグ可能
        selectHelper: true, // これをtrueにすると範囲設定できます
        selectMinDistance: 1 // 説明し難いので別途
    });
  });
</script>

「selectMinDistance」だけこちらで説明します。
fullcalendarさんはクリックイベントもドラッグイベントも取れるのですが、「selectMinDistanceに設定した間を超えたらドラッグイベントで扱うね」という設定らしいです。
これを設定しておかないと、クリックとドラッグでそれぞれ上手くイベントが取れなかったり意図しない動きになるので設定しておくといいです。
単位は何だったのか忘れてしまいました。秒じゃないとは思うのですが。
とりあえず「1」にしておけばいいと思います。適当ですいません。

fullcalendarのイベント設定

これは上に書いたプロパティ設定に続けて書くことにします

<script>
  $(document).ready(function() {
    $('#calendar').fullCalendar({
        header: {
          left: 'prev,next today',
          center: 'title',
          right: 'month,agendaWeek,agendaDay,listMonth'
        },
        timeFormat: 'HH:mm',
        timezone: 'Asia/Tokyo', 
        eventLimit: true, 
        editable: true, 
        slotEventOverlap: true, 
        selectable: true, 
        selectHelper: true, 
        selectMinDistance: 1, 
        events: function(start, end, timezone, callback) {
           // ページロード時に表示するカレンダーデータ取得イベント
        },
        eventClick: function(calEvent, jsEvent, view) {
           // カレンダーに設定したイベントクリック時のイベント
        },
        dayClick: function(date, jsEvent, view) {
           // カレンダー空白部分クリック時のイベント
        },
        select: function(start, end) {
           // カレンダー空白部分をドラッグして範囲指定した時のイベント
        },
        eventDrop: function(event, delta, revertFunc, jsEvent, ui, view) {
           // イベントをドラッグして別日に移動させた時のイベント
        }
    });
  });
</script>

パッと見がそれっぽくなってきました。
上に書いたコメント部分が重要なやつです。
あとはこの部分でAjax呼べばOKです。

何やら長くなってきてしまったので、一旦ここで切ってみます。
続きは次回の記事で書きます。

稼働中のメールサーバ経由で、別サーバからメール送信してみた。

偉そうにサーバ構築なんてやっています。
まぁ常にタコ糸で綱渡りしているのは間違い無いのですが。
カンニングペーパー代わりのEvernoteが成長しています。

さて。
今回は偉そうなことをやってみました。

やりたかったこと

ConoHaで立てた稼働中のメールサーバ経由で、別サーバからメール送信する必要が出て来ました。
別サーバというのはWordpressのサーバなので、プラグイン使ってポイーっとメール送信すればいいのですが、そのサーバ上にランディングページ用の別ディレクトリを切って、そこに配置したお問い合わせフォームからメール送信という仕様です。

え。普通にWordpressに置けばいいじゃん。

とも思ったのですが仕方がありません。
あまり時間もなかったので、お問い合わせフォームはこちらを使わせて頂きました。

www.php-factory.net

どうもありがとうございます。

ちょっと考えてみた

アレです。
要はフォームが載っているサーバから、メールサーバの送信情報を使ってサクっとやればいいのです。
かんたん。かんたーん。

と思ったのが間違いでした。
よく思い出せばよかったのです。自分はインフラが嫌いだということを。
ここ最近、サーバをさわる機会が多かったので勘違いしていました。

以下、このように記載することにします。

・サーバA:Wordpressが載っているサーバ
・サーバB:メールサーバ(固定IPアドレスを振っています)

と、操作はrootでやってます。
OSはCentOS7です。
apacheは2.4です。

とりあえずsendmailをインストールしてみた

既に入っていればいいですが、入ってなかったらインストールしましょう。
CentOSって最初から入ってなかったっけ、などと思いながらyumちゃんにお願いしました。

$ yum install sendmail
$ yum install sendmail-cf

yumちゃんがいい感じにインストールしてくれます。
sendmail-cfの方は後で使うかもしれませんので、まとめて入れてしまいましょう。
ちなみに今回は使ってません。

何となくサーバAのホスト名を変更してみた

何かよくわかりませんが、ホスト名を変更してみました。

$ hostnamectl set-hostname xxxxx.com

上記のコマンドでホスト名が変わります。
変わるのですがこのままだとちょっと都合が悪いので、以下の処理も続けて実施します。

$ vim /etc/cloud/cloud.cfg

# 「- update-hostname」部分をコメントアウト
# - update-hostname

やってやりました。
ホスト名なのですが、自分の場合「メールドメインWordpressサーバドメインが同一」だったので、こちらのホスト名は別の名前に変えてしまいました。
srv.xxxxxxx.comみたいな感じです。

SMTP送信設定をしてみた

いよいよコアな部分です。ドキドキします。
sendmailをインストール後、「/etc/mail/」配下のファイルをごにょごにょします。

ここにsendmailの設定ファイルの数々がいらっしゃいます。
次にSMTP送信設定用の「authinfo」とかいうそれっぽい名前のファイルを開きましょう。
以降のvimコマンドなどは、こちらのディレクトリ内で叩いているという解釈でお願いします。

$ vim /etc/mail/authinfo

開いたらここにSMTP送信設定を記載します。
SMTP送信設定はメールサーバの方にきっとあることでしょう。
2行でセットです。

AuthInfo:SMTPサーバホスト名 "U:メール送信元の対象アドレス" "P:メール送信元の対象アドレスパスワード" "M:PLAIN"
AuthInfo:SMTPサーバホスト名:ポート番号 "U:メール送信元の対象アドレス" "P:メール送信元の対象アドレスパスワード" "M:PLAIN"

SMTPサーバホスト名部分がIPアドレスでもOKなのかは試してないです。
まぁホスト名をブチ込んでおけばいいんじゃないかと。
ConoHaのメールサーバの場合はサーバ詳細の部分に書いてあります。
多分、以下のような感じになるかと思います。

AuthInfo:smtp.xxxxx.aaaaaa.com "U:info@xxxxxx.com" "P:abcdefg12345" "M:PLAIN"
AuthInfo:smtp.xxxxx.aaaaaa.com:587 "U:info@xxxxxx.com" "P:abcdefg12345" "M:PLAIN"

ポート番号は大部分が「587」だとは思いますが、メールサーバに合わせて適宜修正して下さい。

sendmail.mcを修正して、sendmail.cfを作成することにした

ボスの登場です。
こいつが手強かったです。
が、以下のような感じの修正でできると思います。

sendmailの送信にはsendmail.cfを使用するのですが、sendmail.mcファイルを修正してcfファイル(コンフィグファイルらしいです)を生成するのが一般的だそうです。

$ vim /etc/mail/sendmail.mc

## 以下の設定を編集して下さい。
## コメントアウトするものと追記が必要なものは一応書いてみました

define(`confAUTH_OPTIONS', `A')dnl #コメント解除
define(`confAUTH_MECHANISMS', `EXTERNAL GSSAPI DIGEST-MD5 CRAM-MD5 LOGIN PLAIN')dnl #コメント解除
TRUST_AUTH_MECH(`EXTERNAL GSSAPI DIGEST-MD5 CRAM-MD5 LOGIN PLAIN')dnl #追記する
FEATURE(`authinfo', `Hash -o /etc/mail/authinfo')dnl #追記
define(`SMART_HOST', `SMTPサーバホスト名')dnl #コメント解除して編集
define(`RELAY_MAILER_ARGS', `TCP $h 587')dnl #追記
define(`ESMTP_MAILER_ARGS', `TCP $h 587')dnl #追記
dnl DAEMON_OPTIONS(`Port=smtp,Addr=127.0.0.1, Name=MTA')dnl #コメントアウト
DAEMON_OPTIONS(`Port=smtp, Name=MTA')dnl #追記
LOCAL_DOMAIN(`設定したサーバAのホスト名')dnl #コメント解除して編集
MASQUERADE_AS(`メール送信するサーバBのドメイン名')dnl #コメント解除して編集
FEATURE(masquerade_envelope)dnl #コメント解除

雑ですみません。
.mcファイルさんはコメントアウトに「dnl」という謎の文字列を使うそうです。
見慣れませんが仕方がないです。

accessファイルを編集してみる

このファイルはメールのリレー設定に使うファイルです。
否認とかにも使えます。
その辺はGoogle先生がきっと教えてくれます。

今回はサーバAから送信する時、サーバBを使いたいだけなのでリレー設定だけです。
偉そうに書いてる風味ですが、あまりよくわかっていません。

$ vim /etc/mail/access

Connect:サーバBのIPアドレス  RELEY

きっと「指定したIPアドレスにリレーしますよ!」っぽい何かだと思います。
多分。

mailertableとかいうものも編集してみる

これは「送信元のメールドメインはここですよ!」ファイルだと思われます。
そんな感じでいいんじゃないですかね!

$ vim /etc/mail/mailertable

送信元のメールドメイン  smtp:SMTPサーバホスト名

実際の記載例だとこんな風になると思います。

sample.com  smtp:smtp.sample.com

smtp:〜部分はダブルクォーテーションなどで囲わないようにして下さい。
何か上手くいかなかった模様です。

できたっぽい気がするのでsendmailを再起動してみる

上記の設定を反映させるために再起動します。
ひとつひとつの設定からファイルを生成する方法もあるのですが、ボスファイルである「sendmail.mc」の方に再起動したら新しいファイル作ってやるぜ設定が書いてあるので、sendmail再起動でいいかと思います。

$ systemctl restart sendmail

再起動コマンドはOSによって違うと思いますので、その辺はフィーリングで。

ついでにapacheも再起動しておきました。

$ systemctl restart httpd

うまく設定できたか試してみた

と、上記の設定が上手くできていればいいなと祈りながら、以下のコマンドを打ってみましょう。

$ sendmail -bv メールアドレス
# 実際には送信されません
# sendmail -bv aaaaa@sample.com みたいな感じでコマンドを打ちます

これを実行して上手いことできていると、以下のようなログが流れます。

$ sendmail -bv aaaaa@sample.com
aaaaa@sample.com... deliverable: mailer smtp, host smtp.xxxxxx.sample.com, user aaaaa@sample.com

で き て い る

そもそも何故こんなことをしたのか

問い合わせが入った時、管理側と投稿者に「問い合わせきたよー」メールを送信したかったのです。
投稿者の方は問題ないのですが、管理側の方が上手くいきませんで。
よくよく考えたら、ローカルホストに対してローカルホストユーザにメールをするわけなので、そのユーザがいないと「User unknown」と無慈悲なエラーログが表示されてしまいます。

で、今回やってみたのですが果たしてこれでいいのか。
少々不安なのですが、概ねいいでしょう。