プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

はてなブログのコードハイライトを SyntaxHighlighter から はてなの markdown に変更

はてなブログに移行してくる前から、Syntaxhiglighter を使っていたんですが、どうもここしばらく開発が止まっているようです。

github.com

一応、こちらの記事に書いたように v4 に対応させて使っていたんですが、最近手を出している新しめの言語、 dart (flutter で利用) とかに対応していないのが、気になってきました。 それと、スマホには対応していないんですよねー。

そんなことが重なったので、そろそろ替え時かな?と思うようになりました。 というわけで、 SyntaxHighlighter から はてなブログが提供している markdown によるコードハイライトに切り替えようと思い立ちました。

ただ、既存の記事が多数あるので手作業は嫌です。幸い、はてなブログでは、API経由で投稿・編集ができるので、それを使って、らくちんで対応したいと思います。

というわけで、移行時に行った作業のメモをまとめておきます。

API について

まず、はてなのAPIでできることを調べてみます。

はてなブログAtomPub - Hatena Developer Center

エントリー一覧の取得、個別の記事の取得、新規投稿およびアップデートと一通りのことができますね。

wget を使ってエントリー一覧をとってみます。認証は Basic 認証です。

mor@DESKTOP-H6IEJF9:~/work/hatena$ wget --user=はてなID --password=APIキー https://blog.hatena.ne.jp/はてなID/ブログID/atom/entry -O test.xml
Will not apply HSTS. The HSTS database must be a regular and non-world-writable file.
ERROR: could not open HSTS store at '/home/mor/.wget-hsts'. HSTS will be disabled.
--2020-12-23 10:15:42--  https://blog.hatena.ne.jp/はてなID/ブログID/atom/entry
blog.hatena.ne.jp (blog.hatena.ne.jp) をDNSに問いあわせています... 13.115.18.61, 13.230.115.161
blog.hatena.ne.jp (blog.hatena.ne.jp)|13.115.18.61|:443 に接続しています... 接続しました。
HTTP による接続要求を送信しました、応答を待っています... 401 Unauthorized
選択された認証形式: Basic realm="Hatena Blog"
blog.hatena.ne.jp:443 への接続を再利用します。
HTTP による接続要求を送信しました、応答を待っています... 200 OK
長さ: 特定できません [application/atom+xml]
`test.xml' に保存中

test.xml                                                    [ <=>                                                                                                                         ] 187.44K  --.-KB/s    時間 0.04s

2020-12-23 10:15:42 (4.57 MB/s) - `test.xml' へ保存終了 [191940]

mor@DESKTOP-H6IEJF9:~/work/hatena$

こんな感じで XML が返ってきました。

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:app="http://www.w3.org/2007/app">

  <link rel="first" href="https://blog.hatena.ne.jp/はてなID/ブログID/atom/entry" />


  <link rel="next" href="https://blog.hatena.ne.jp/はてなID/ブログID/atom/entry?page=xxxxxxxxxxx" />


  <title>プログラマーのメモ書き</title>

  <subtitle>伊勢在住のプログラマーが気になることを気ままにメモったブログです</subtitle>

  <link rel="alternate" href="https://blog.mori-soft.com/"/>
  <updated>2020-12-17T17:20:51+09:00</updated>
  <author>
    <name>junichim</name>
  </author>
  <generator uri="https://blog.hatena.ne.jp/" version="4d0fe3f3cd3a840afb05e29c86af788f">Hatena::Blog</generator>
  <id>hatenablog://blog/yyyyyyyyyyyyyyyyyyyyyyy</id>


  <entry>
<id>tag:blog.hatena.ne.jp,2013:blog-はてなID-xxxxxxxxxxxxxxxxxxxxxx-yyyyyyyyyyyyyy</id>
<link rel="edit" href="https://blog.hatena.ne.jp/はてなID/ブログID/atom/entry/エントリーID"/>
<link rel="alternate" type="text/html" href="https://blog.mori-soft.com/entry/2020/12/23/002700"/>
<author><name>junichim</name></author>
<title>はてなブログのコードハイライトを SyntaxHighlighter から はてなの markdown に変更</title>
<updated>2020-12-21T16:02:21+09:00</updated>
<published>2020-12-18T10:55:34+09:00</published>
<app:edited>2020-12-23T00:27:00+09:00</app:edited>
<summary type="text">はてなブログに移行してくる前から、Syntaxhiglighter を使っていたんですが、どうもここしばらく開発が止まっているようです。 github.com 一応、こちらの記事に書いたように v4 に対応させて使っていたんですが、>最近手を出している新しめの言語、 flutter…</summary>
<content type="text/x-markdown">はてなブログに移行してくる前から、Syntaxhiglighter を使っていたんですが、どうもここしばらく開発が止まっているようです。


[https://github.com/syntaxhighlighter/syntaxhighlighter:embed:cite]

一応、[こちらの記事](https://blog.mori-soft.com/entry/2018/01/31/224619)に書いたように v4 に対応させて使っていたんですが、最近手を出している新しめの言語、 flutter (dart) とかに対応していないのが、気になってきました。
それと、スマホでみると、シンタックスハイライトがうまく機能していません。

これだけではちょっとわかりにくいですが、エントリーIDの一覧が返ってくるかと思いきや、一度に複数件(これ試したときには10件、ドキュメント上は7件)の記事が完全な形で取得されています。

で、次の一覧を得るためには、取得したドキュメント内のlink要素(rel属性がnextのやつ)のURLを呼ぶという形になるようです。 pageパラメータの求め方がわからないので、順番にコレクションを取得して、次のURLを呼び、得られたコレクションにある次のURLを呼び・・・という形を用いて、すべてのエントリーを得ることができるようです。

page パラメータをダイレクトに求められれば、もうちょっと楽な気がします。

個別の記事については、entry タグに含まれている link タグから、エントリーIDを切り出せば、アップデートに使えそうです。

こういう処理は Python でやると楽そうですね。なかなか本格的に Python を使う機会もないので、手元の書籍を見ながらボチボチ組んでみます。

Python スクリプトの作成

まず、コードを書く前に、 SyntaxHighlighter での言語指定と はてなブログの markdown の言語指定の対応を調べる必要があります。

とはいっても、すべての言語の対応表を作るなんて、ばからしいので、今回はこういう風に対応することにしました。

  1. 既存の SyntaxHighlighter の言語指定を抜き出し一覧にする
  2. はてなブログの markdown で対応している言語名との対応表を作る
  3. できた対応表を用いて、 SyntaxHighlighter の記述を markdown に変換する

また、全体の処理の流れとしては、

  1. コレクションAPI にてエントリー一覧(10件分)を取得
  2. 各エントリーについて以下を実施
  3. XML をパース
  4. SyntaxHighlighter の記述があるか判定
  5. なければ、次のエントリーへ
  6. あれば、必要な処理を行う(SyntaxHighlighter で指定されている言語の取得や記述の変換)

という感じでしょうか。

API からデータを取得

Python で http 経由で何かを取得するのは urllib が楽なようです。また、はてなのAPIは認証が必要です。今回は、 Basic 認証にします。

下記記事などを参考に、 urllib から Basic 認証で API をたたいて、XML を取得します。

最初に試したとき、APIの最後にスラッシュが付いていたのに気づかず、404 が返ってきてしまい、なんでだ?となってしまいました。お気を付けください。

XML の解析

はてなのAPI からは XML が返ってきます。 Python で XML を操るには、 ElementTree を使うのが王道っぽいですね。

まずは、パースして、個別のエントリーを取得すればよさそうです。

ただ、はてなの API で返ってくる XML は名前空間が指定されているので、そこだけ、工夫してやります。

XML の名前空間はこんな感じです。

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:app="http://www.w3.org/2007/app">

デフォルト名前空間も含めて、下記のような名前空間の辞書を作ります。

# ドキュメント XML の namespace
ns = {'def': 'http://www.w3.org/2005/Atom', 'app': 'http://www.w3.org/2007/app'}

ElementTree の find などを使うときは、この名前空間を指定してやります。例えば、 entry タグをとりたければ、

        self.__entries = root.findall('def:entry', ns)

のようにデフォルト名前空間も明示的に指定してやります。

これで、名前空間付きでタグとかが取れるようになりました。

(参考)

エントリーの取得処理

ここまでをまとめて、このデータ取得部分を含めたクラスを作って、イテレータでアクセスできるようにしました。

こんな感じですね。

# ドキュメント XML の namespace
ns = {'def': 'http://www.w3.org/2005/Atom', 'app': 'http://www.w3.org/2007/app'}

class HatenaEntries():
    '''
    はてなブログのエントリー(記事)を取得し、イテレータで扱うためのクラス
    '''
    def __init__(self):
        self.__isFirst = True
        self.__count = 0
        self.__index = None
        self.__entries = None

    def __iter__(self):
        return self

    def __next__(self):
        # 最初の呼び出し
        if self.__isFirst:
            self.__isFirst = False
            url = self.__getFirstpageUrl()
            self.__getCollectionFromHatena(url)

        if self.__entries is not None and self.__index == len(self.__entries) - 1:
            # 次のコレクションを設定
            self.__getCollectionFromHatena(self.__nextUrl)

        if self.__entries is None:
            raise StopIteration()

        self.__index += 1
        return self.__entries[self.__index]

    def __getCollectionFromHatena(self, url):
        if url is None:
            self.__entries = None
            self.__index = None
            self.__nextUrl = None
            return

        root = self.__getEntryCollection(url)
        self.__entries = root.findall('def:entry', ns)
        if self.__entries is None:
            self.__index = None
        else:
            self.__index = -1
            self.__count += len(self.__entries)
        self.__nextUrl = self.__getNextpageUrl(root)

    def __getFirstpageUrl(self):
        url = os.path.join(BASEURL, ENTRY_PATH)
        return url

    def __getNextpageUrl(self, root):
        for child in root.findall('def:link', ns):
            if child.get('rel') == 'next':
                return child.get('href')
        return None

    def __getEntryCollection(self, url):
        basic = base64.b64encode('{}:{}'.format(USER,PASSWORD).encode('utf-8'))
        headers={"Authorization": "Basic " + basic.decode('utf-8')}

        print("request url: {}".format(url))
        req = urllib.request.Request(url, headers=headers)
        try:
            with urllib.request.urlopen(req) as res:
                data = res.read()
        except urllib.error.HTTPError as err:
            print("exception occured to get entry collection");
            print(err)
            return None
        except urllib.error.URLError as err:
            print("failed connection to server");
            print(err)
            return None

        # XML に変換
        root = ET.fromstring(data)
        return root

    def getCurrentArticles(self):
        '''
        現在までに読み込んだエントリー数
        '''
        return self.__count

SyntaxHighlighter の記述の検出と言語の抽出

エントリーが取れるようになったので、次は正規表現で対象があるかをチェックします。

SyntaxHighlighter は

<pre class="brush: 言語名">

という記述をするのが基本なのですが、過去の経緯もあり、いくつかの表記パターンがありました。

<pre class="brush: 言語名">
<pre class="brush: 言語名" data-unlink="">
<pre class="brush: 言語名",  data-unlink="">
<pre class="brush: 言語名;">
<pre class='brush: 言語名'>

これらをカバーするような正規表現を作ってみます。

pattern = re.compile('(<\s*pre\s+class\s*=\s*(?P<quot>[\"\']?)\s*brush\s*:\s*([\d\w]+)\s*;?\s*(?P=quot)\s*,?\s*(?:data-unlink\s*=\s*(?:\"\"|\'\'))?\s*>)(.+?)(</\s*pre\s*>)', re.MULTILINE | re.DOTALL)

コード部分が複数行にわたるので、 MULTILINE と DOTALL を指定した処理にしています。 また、開始preタグ、言語名、コード本体、終了preタグをグループにして取得できるようにしています。 あとは、空白の有無とかクォーテーションの違いとか、各記号・data-unlink表記の有無に対応できるように作ってみました。

とはいえ、いきなりこの正規表現を作るのはたいへんでしたので、下記サイトなどで、チェックしながら作りました。

regex101.com

言語一覧の取得

上記で取得できるようになった各エントリーに対して、こんな感じでチェックしてやります。

import hatena_entries as HE


# 記事情報の取得
#   isLang, true: 言語一覧, false: 記事タイトル一覧
def listup(entry, isLang):
    global num_convert

    content = entry.find('def:content', HE.ns)
    title = entry.find('def:title', HE.ns)

    resultSet = set()
    # content に syntaxhighlighter の記述 があるか
    if content is not None and content.text is not None:
        m = pattern.findall(content.text)

        if len(m) > 0:
            num_convert += 1
            if isLang:
                for v in m:
                    resultSet.add(v[2]) # #3 lang
            else:
                resultSet.add(title.text)

    return resultSet

set(集合)を利用して、言語名の重複を除いてやります。

取得した言語名一覧と対応表

最終的に作成した言語名一覧と対応表は下記のような辞書にまとめて、変換処理で使うようにしました。

# 言語指定の変換一覧
#   key: SyntaxHighlighter
#   val: はてなブログ markdown
lang= {
    "actionscript3": "actionscript",
    "bash": "sh",
    "cpp": "cpp",
    "dart": "dart",
    "html": "html",
    "java": "java",
    "javascript": "javascript",
    "js": "javascript",
    "json": "javascript",
    "php": "php",
    "python": "python",
    "scala": "scala",
    "sql": "sql",
    "text": "",       # text には単なるコードブロックを対応させる
    "vb": "vb",
    "xml": "xml",
    "yaml": "yaml",
}

SyntaxHighlighter の記述を markdown へ変換

SyntaxHighlighter のタグさえ見つけられれば、変換はさほど難しくありません。

こんな感じで、変換関数を定義して変換します。

def replacer(matchObj):
    nl1 = "" if matchObj.group(4)[0] == "\n" else "\n"
    nl2 = "" if matchObj.group(4)[-1] == "\n" else "\n"
    # 問題再現
    #nl1 = ""
    #nl2 = ""

    return "```" + codeLang.lang[matchObj.group(3)] + nl1 + matchObj.group(4) + nl2 + "```"

def getConvertedContent(content_text):
    return re.sub(pattern, replacer, content_text)

実は、上記は正しく動作するのですが、当初は下記のように pre タグを置き換えるだけにしてしまいました。

def replacer(matchObj):
    return "```" + lang[matchObj.group(3)] + matchObj.group(4) + "```"

すると、開始preタグの後ろに改行なしでコード本体が書かれている場合に、言語名とコード本体がつながってしまうという事態になってしまいます。

この記述で問題ないと思い込んでしまったため、本番のブログへも反映してしまい、修正作業が大変になってしまいました。テストは大事ですね。 修正についても、後で述べます。

更新作業

更新はエントリーIDさえ、わかれば簡単です。

def updateEntry(entry_id, entry_xml):
    '''
    エントリーの更新
    '''
    url = os.path.join(BASEURL, ENTRY_PATH + '/' + entry_id)

    basic = base64.b64encode('{}:{}'.format(USER,PASSWORD).encode('utf-8'))
    headers={
        "Authorization": "Basic " + basic.decode('utf-8'),
        "Content-type": "application/xml",
    }

    print("request url: {}".format(url))
    req = urllib.request.Request(url, headers=headers, data=entry_xml.encode('utf-8'), method='PUT')
    try:
        with urllib.request.urlopen(req) as res:
            data = res.read()
    except urllib.error.HTTPError as err:
        print("exception occured for update, entry id: {}".format(entry_id));
        print(err)
        print("continue next entry")
    except urllib.error.URLError as err:
        print("failed connection to server");
        print(err)
        print("continue next entry")

なお、当初は method として小文字の put にしていたら、404 が返ってきて、ずいぶんと悩みました。大文字にしたら問題なく更新できました。

実行

スクリプトが完成したので、実行してみます。

まずは言語一覧を取得します。

mor@DESKTOP-H6IEJF9:~/work/hatena$ python3 convert_syntax.py -g
(略)
num of articles: 326
num of articles to convert: 164

actionscript3
bash
cpp
dart
html
java
javascript
js
json
php
python
scala
sql
text
vb
xml
yaml
mor@DESKTOP-H6IEJF9:~/work/hatena$

で得られた結果を Python コードに反映して、変換処理を行います。

mor@DESKTOP-H6IEJF9:~/work/hatena$ python3 convert_syntax.py -c
(略)
num of articles: 326
num of articles to convert: 164

mor@DESKTOP-H6IEJF9:~$

エラーがでなければ処理完了です。

変換結果

変換した結果は、例えば、こちらの記事のやつだと、

f:id:junichim:20201220002157p:plain

が、

f:id:junichim:20201223112500p:plain

のようになります。

コードハイライトの行番号とかは、カスタマイズ方法がネットにいろいろとあるので、また別途対応したいと思います。

確認と修正

変換した全ての記事を見るわけにはいかないので、いくつかの記事を調べて、取りこぼしや変換ミスがないかチェックします。

正規表現を下記のように変更して

pattern = re.compile('<\s*pre\s+class\s*=\s*[\"\']?\s*brush', re.MULTILINE | re.DOTALL)

タイトル一覧を取得して、差分をチェックします。

増えた記事があったら、変換漏れなので、対応を検討します。今回は、以前のブログよりインポートした記事が、編集モードは markdown なのに、中身は html で記述されていて、そのため、<pre class="brush: " タグを正しく検出できていないというものがありました。なので、これについては、記事そのものを直接修正しました。

言語名とコード本体が結合した問題

で、今回の変換でやらかしたのがこれです。

SyntaxHighliter の記述は html タグでの指定なので改行がなくてもいいことを完全に忘れてました。 仕方ないので、これの修正スクリプトも作りました。正規表現を

pattern_pre = re.compile('^(```)(.+)$', re.MULTILINE)
pattern_post = re.compile('^(.+)(```)$', re.MULTILINE)

のように開始部分と終了部分に対応できるように用意します。

変換処理は、開始部分と終了部分で処理を分け、開始部分はトリプルバッククォートに続く文字が、言語辞書に含まれているか文字か否かをチェックして、改行位置を切り替えています。

def pre_replacer(matchObj):
    langs = set([lng for lng in clang.lang.values() if len(lng) > 0])
    for lng in langs:
        if matchObj.group(2).startswith(lng):
            pre = matchObj.group(1) + lng
            content = matchObj.group(2)[len(lng):]
            if len(content) > 0:
                return pre + "\n" + content
            else:
                return pre
    return matchObj.group(1) + "\n" + matchObj.group(2)

def post_replacer(matchObj):
    return matchObj.group(1) + "\n" + matchObj.group(2)

def fixPreMarkdown(content_text):
    return re.sub(pattern_pre, pre_replacer, content_text)

def fixPostMarkdown(content_text):
    return re.sub(pattern_post, post_replacer, content_text)

これを、開始タグの修正->終了タグの修正と呼ぶことで、1行で開始タグと終了タグが含まれている場合にも対応できます。

これを実行して、なんとかおおむね修正できました。

html エスケープ文字の修正

SyntaxHighlighter v4 では pre タグで囲まれたコード本体に、 < や > などがあるときは html エスケープしないといけませんでした。 このままだと、markdown方式に変換すると、エスケープした文字がそのまま表示されてしまいます。

これについては、ちょっと面倒ですが、エスケープ文字を含むエントリー一覧を作成して、順次直しました。

これが一番大変でしたね。

br タグの修正

同様に、Syntaxhighlighter のコード本体に br タグが含まれる場合もあります。 これも同様に対象となるエントリー一覧を作成して、順次直しました。

まとめ

まだ、修正による不具合があるかもしれませんが、順次見つけ次第直すことにします。しかし、まあ、意外と修正するところが多くて、予想外に大変でした。変換時に改行コードを入れ忘れるというミスがなければもうちょっと楽だったかな?

似たような作業する方がいれば何かの参考になればと思います。

なお、完成したスクリプト全体は Gist に置いておきますので、ご興味のある方はご自由にお使いください(WTFPL ライセンスです)。

参考:既存のブログのコピーを作成

テスト用に、ブログの記事を一旦エクスポートして、インポートして作業も行いました。

この際、気づいたのですが、インポート後は、編集モードが『見たまま編集』になっています。前述したように、今のブログも別のところからインポートしてきましたが、編集モードはmarkdownです。どういうことだ?仕様が変わったのか?

と思い調べてみると、はてな公式のアナウンスはありませんが、どうも、

  • MovableType形式でインポート:みたまま編集モード
  • WordPress形式でインポート:markdown

となるようです。下記の事例の記事から推測しました。

ご参考まで。

追記

2020/12/28 追記

SyntaxHighlighter を使わなくなったので、こちらの記事で設定した、cssおよびjsファイルの読み込み設定(はてな側の設定)を削除しました。

Rainloop の upgrade, 1.13.0 -> 1.14.0

rainloop のバージョンを 1.13.0 から 1.14.0 にアップグレードしたので、手順をメモっておきます。

なお、公式の説明は、下記のとおりです。

Upgrade / Documentation /RainLoop Webmail

(必要があれば) EBS ボリュームのスナップショットを作成

現在、rainloop を EC2 で動かしているので、トラブル時に復旧できるよう、EBSボリュームのスナップショットを作成しておきます。

アップグレード作業

最悪の場合、元に戻せるようにバックアップを取っておきます。

ubuntu@ip:~$ mkdir tmp/rainloop_bak
ubuntu@ip:~$ cp -p -R /var/www/html/rainloop tmp/rainloop_bak

アップグレードしたいバージョンを取得します。

ubuntu@ip:~$ wget https://github.com/RainLoop/rainloop-webmail/releases/download/v1.14.0/rainloop-community-1.14.0.zip

解凍して

ubuntu@ip:~$ mkdir tmp/rainloop_1.14.0_test
ubuntu@ip:~$ cd tmp/rainloop_1.14.0_test
ubuntu@ip:~/tmp/rainloop_1.14.0_test$ unzip ../../rainloop-community-1.14.0.zip 

コピー

ubuntu@ip:~$ 
ubuntu@ip:~$ cd tmp/rainloop_1.14.0_test/
ubuntu@ip:~/tmp/rainloop_1.14.0_test$ 
ubuntu@ip:~/tmp/rainloop_1.14.0_test$ sudo cp -p -R ./* /var/www/html/rainloop/

所有者を修正

ubuntu@ip:~/tmp/rainloop_1.14.0_test$ cd /var/www/html/rainloop/
ubuntu@ip:/var/www/html/rainloop$ sudo chown -R www-data:www-data ./*

webからアクセスして、管理画面を表示すると、

f:id:junichim:20201217154658p:plain

バージョン番号が更新されているのがわかります。

簡単ですね。

設定ファイルのバックアップ

ついでに、設定ファイルのバックアップを取るようにします

こんなシェルスクリプトを ~/bin/rainloop_settings_backup.sh という名前で作っておきます。

#!/bin/bash
#
# rainloop の設定情報をバックアップ用にまとめる
#
# written by Junichi MORI, 2020/12/17

#
# rainloop's dir
#
RAINLOOP_DIR=/var/www/html/rainloop
TARGET_DIR=data/
# SETTINGS_DIR is a relative path from TARGET_DIR
SETTINGS_DIR=_data_/_default_
CACHE_DIR=${SETTINGS_DIR}/cache
LOGS_DIR=${SETTINGS_DIR}/logs

# アーカイブファイル名
ARCHIVE_PRFX=rainloop_settings
ARCHIVE_SUFX=".tgz"
ARCHIVE_FILE=${ARCHIVE_PRFX}_`date '+%Y%m%d'`${ARCHIVE_SUFX}

# 設定情報のバックアップ
echo "tar zcvf ${ARCHIVE_FILE} -C ${RAINLOOP_DIR} --exclude=${CACHE_DIR} --exclude=${LOGS_DIR} ${TARGET_DIR}"
tar zcvf ${ARCHIVE_FILE} -C ${RAINLOOP_DIR} --exclude=${CACHE_DIR} --exclude=${LOGS_DIR} ${TARGET_DIR}

rainloop のキャッシュディレクトリは、

data/_data_/_default_/cache
 

ログディレクトリは

data/_data_/_default_/logs
 

なので、これらを除外するようにしています。

※ キャッシュディレクトリの場所は、下記の issues のディスカッションで述べられていました

実行しておきます。

ubuntu@ip:~$ ./bin/rainloop_settings_backup.sh

できたファイルを保存しておけばOKです。

Rainloop の設定変更

こちらの記事に書いたように、メールアカウントの POP3 -> IMAP4 移行に伴い、Rainloop を導入しました。

で、しばらく使って、困ったことがあり、それに伴いいくつか設定を見直したので、メモとして残しておきます。

※ なお、実はこの記事の内容は 2019/11/6 時点での設定で、ブログに投稿しようと思って、大雑把にメモってそのまま忘れ去られていました。先日、下書きが残っているのに気づいたので、改めて公開した次第です。

Rainloop のバージョンは v1.13.0 になります。

多数のフォルダの処理ができない

フォルダ数が多いと下記のような警告が出て、表示されていないフォルダの操作ができなくなりました。

f:id:junichim:20191105140503p:plain

解決策

下記にあるように application.ini の設定値を変更すれば対応できます。

Where to set limit for "You have too many folders!" option · Issue #978 · RainLoop/rainloop-webmail · GitHub

application.ini は rainloop/data/_data_/_defaul_/configs にあります。

修正前

[labs]
; Experimental settings. Handle with care.
; 
(中略)
imap_folder_list_limit = 200
(後略)

修正後

[labs]
; Experimental settings. Handle with care.
; 
(中略)
imap_folder_list_limit = 300
(後略)

フォルダ数が多い場合は、フォルダ毎の未読がチェックされない

Rainloop ではフォルダ数が 50 (設定ファイルで変更可能です)以上の場合、フォルダ毎の未読数が表示されません(パフォーマンスのため)。

How can the unread message count on folders be refreshed? · Issue #803 · RainLoop/rainloop-webmail · GitHub

このため、フォルダ数が 50 以上の場合に、未読数の表示を有効にするためには、フォルダ管理画面より、『新しいメッセージをチェックする/しない』を設定する必要があります。ちなみに、デフォルトはシステムフォルダ以外はすべてチェックしないとなっています。

[question] Unread messages · Issue #611 · RainLoop/rainloop-webmail · GitHub

こんな感じで設定していきます。

f:id:junichim:20201202104329p:plain

この例の場合だと、 Junk E-mail フォルダは未読数をチェックしませんが、画面内の他のフォルダはチェックになっています。また、太字のフォルダはシステムフォルダです。

なお、すべてのフォルダの未読数を表示する上限は、 application.ini の下記の値を変更すればよいそうです(試してないので、うまくいくかどうかは不明です)。

[labs]
; Experimental settings. Handle with care.
; 
(中略)
folders_spec_limit = 50
(後略)

ログを残すようにする

デフォルトでは、ログは作成されません。ログファイルを残すには、 application.ini の [logs] セクションの設定を変更します。

Activate logging and find logs for {Rainloop in Nextcloud} - PAB's blog

修正前

[logs]
; Enable logging
enable = Off

; Logs entire request only if error occured (php requred)
write_on_error_only = Off

; Logs entire request only if php error occured
write_on_php_error_only = Off
(後略)

修正後

[logs]
; Enable logging
enable = On

; Logs entire request only if error occured (php requred)
write_on_error_only = On

; Logs entire request only if php error occured
write_on_php_error_only = On
(後略)

先頭の enable = On でログが有効になります。その他の設定は、エラー時のログだけ残したいので設定しました。ほかにも設定オプションがあるので、気になる方はご自分で調べてみてください。

ちなみに、ログファイルの保存先は、 rainloop/data/_data_/_default_/logs フォルダ以下になります。

管理画面のURL

Rainloop の管理画面のURLですが、デフォルトだと、

https://サーバー名/?admin

になっています。でも、デフォルトのままだと不安ですよね?これも変更できます。

admin panel security · Issue #976 · RainLoop/rainloop-webmail · GitHub

変更方法は application.ini の admin_panel_key を変更すればOKです。

修正前

[security]
; Enable CSRF protection (http://en.wikipedia.org/wiki/Cross-site_request_forgery)
(中略)
; Access settings
admin_panel_key = "admin"

修正後

; Enable CSRF protection (http://en.wikipedia.org/wiki/Cross-site_request_forgery)
(中略)
; Access settings
admin_panel_key = "my-new-admin-url-query"

変更後は、https://サーバー名/?my-new-admin-url-query でアクセスして表示されるログイン画面から、正しいユーザー名とパスワードを入れることでログインできるようになります。

なお、 https://サーバー名/?admin でもログイン画面が表示されますが、正しいユーザー名とパスワードを入れてもログインできなくなります。

添付ファイルサイズ制限を緩める

デフォルトはこれでした。

f:id:junichim:20191112090416p:plain

Rainloop 側の制限は 25M でしたが、PHP側でアップロード 2M, ポスト 8M の制限がかかっています。

なので、php側の制限を変更して、大きめのファイルを送信できるようにします。

php.ini を編集する必要があるので、もし、どの php.ini を使っているのか不明であれば、管理画面の『セキュリティ』にある『PHP情報を表示』からphpinfoの情報を見て確認します。

f:id:junichim:20201202111644p:plain

『Loaded Configuration File』をみれば、現在読み込んでいるphp.iniの場所がわかります。

修正前

; Maximum size of POST data that PHP will accept.
; Its value may be 0 to disable the limit. It is ignored if POST data reading
; is disabled through enable_post_data_reading.
; http://php.net/post-max-size
post_max_size = 8M

および

; Maximum allowed size for uploaded files.
; http://php.net/upload-max-filesize
upload_max_filesize = 2M

修正後

; Maximum size of POST data that PHP will accept.
; Its value may be 0 to disable the limit. It is ignored if POST data reading
; is disabled through enable_post_data_reading.
; http://php.net/post-max-size
post_max_size = 30M

; Maximum allowed size for uploaded files.
; http://php.net/upload-max-filesize
upload_max_filesize = 30M

変更後は、apache を再起動しておきます。

管理画面で確認すると、無事変更されていました。

f:id:junichim:20201202111924p:plain

まとめ

Rainloop フォルダ周りの機能があまりないとか、不満もありますが、一か所でいろんなメールを確認できる環境はなかなか便利です。 IMAP なので、Rainloop 側でのバックアップも設定ファイルを残しておくだけでいいので、そんなに気にしないで済むのもありがたいです。

気になる方は一度試してみるといいかもしれませんよ。