はじめに
必要に迫られてNNTPで通信をすることになりました。
昔に作成した自前のNNTPクラスでも良かったのですが、Gemfileが使えるruby-net-nntpを利用することにします。このライブラリは記事をTMail::Mailクラスに格納するため、bundle installで、tmail-1.2.7.1もダウンロードします。
net/nttpの利用と、tmailで発生した問題について、まとめてみました。
環境 & 準備作業
- ubuntu 18.04 LTS
- ruby 2.5.1 (Ubuntu付属のパッケージ)
ライブラリのダウンロード
次のファイルを準備しました。
source 'https://rubygems.org'
gem "ruby-net-nntp"
$ bundle install --path lib
$ ls lib/ruby/2.5.0/gems/
log4r-1.1.10/ ruby-net-nntp-1.0.0/ tmail-1.2.7.1/
サンプルプログラムの作成
コードを動かそうとしたところ、さっそくエラーに遭遇しました。
遭遇したエラー
require "net/nntp"
を実行すると、次のようなエラーが発生します。
$ irb
irb(main):001:0> require "rubygems"
=> false
irb(main):002:0> require "bundler/setup"
=> true
irb(main):003:0> require 'net/nntp'
Traceback (most recent call last):
14: from /usr/bin/irb:11:in `<main>'
...
2: from .../lib/ruby/2.5.0/gems/tmail-1.2.7.1/lib/tmail/utils.rb:110:in `<module:TMail>'
1: from .../lib/ruby/2.5.0/gems/tmail-1.2.7.1/lib/tmail/utils.rb:117:in `<module:TextUtils>'
RegexpError (/.../n has a non escaped non ASCII character in non ASCII-8BIT script)
これはhttp://www.ownway.info/Blog/2011/09/-invalid-multibyte-escape.html - 正規表現で invalid multibyte escape エラーが発生する問題の対処方法に説明されています。
この記事にあるGoogleへのリンクは機能しないので、https://www.ruby-forum.com/t/ruby-1-9-1-invalid-multibyte-escape-regexperror/164732で、オリジナルの議論が確認できます。
差分
単純に問題が起こる箇所の /../n 記法を、Regexp.new() で置き換えます。
diff -ru tmail-1.2.7.1.orig/./lib/tmail/scanner_r.rb tmail-1.2.7.1/./lib/tmail/scanner_r.rb
--- tmail-1.2.7.1.orig/./lib/tmail/scanner_r.rb 2020-06-10 22:06:16.813972850 +0900
+++ tmail-1.2.7.1/./lib/tmail/scanner_r.rb 2020-06-11 11:17:51.741512220 +0900
@@ -63,24 +63,24 @@
PATTERN_TABLE = {}
PATTERN_TABLE['EUC'] =
[
- /\A(?:[#{atomchars}]+|#{iso2022str}|#{eucstr})+/n,
- /\A(?:[#{tokenchars}]+|#{iso2022str}|#{eucstr})+/n,
+ Regexp.new("\A(?:[#{atomchars}]+|#{iso2022str}|#{eucstr})+", nil, "n"),
+ Regexp.new("\A(?:[#{tokenchars}]+|#{iso2022str}|#{eucstr})+", nil, "n"),
quoted_with_iso2022,
domlit_with_iso2022,
comment_with_iso2022
]
PATTERN_TABLE['SJIS'] =
[
- /\A(?:[#{atomchars}]+|#{iso2022str}|#{sjisstr})+/n,
- /\A(?:[#{tokenchars}]+|#{iso2022str}|#{sjisstr})+/n,
+ Regexp.new("\A(?:[#{atomchars}]+|#{iso2022str}|#{sjisstr})+", nil, "n"),
+ Regexp.new("\A(?:[#{tokenchars}]+|#{iso2022str}|#{sjisstr})+", nil, "n"),
quoted_with_iso2022,
domlit_with_iso2022,
comment_with_iso2022
]
PATTERN_TABLE['UTF8'] =
[
- /\A(?:[#{atomchars}]+|#{utf8str})+/n,
- /\A(?:[#{tokenchars}]+|#{utf8str})+/n,
+ Regexp.new("\A(?:[#{atomchars}]+|#{utf8str})+", nil, "n"),
+ Regexp.new("\A(?:[#{tokenchars}]+|#{utf8str})+", nil, "n"),
quoted_without_iso2022,
domlit_without_iso2022,
comment_without_iso2022
@@ -258,4 +258,4 @@
end
end # module TMail
-#:startdoc:
\ No newline at end of file
+#:startdoc:
diff -ru tmail-1.2.7.1.orig/./lib/tmail/utils.rb tmail-1.2.7.1/./lib/tmail/utils.rb
--- tmail-1.2.7.1.orig/./lib/tmail/utils.rb 2020-06-10 22:06:16.813972850 +0900
+++ tmail-1.2.7.1/./lib/tmail/utils.rb 2020-06-11 11:11:59.582616384 +0900
@@ -114,10 +114,11 @@
lwsp = %Q| \t\r\n|
control = %Q|\x00-\x1f\x7f-\xff|
- CONTROL_CHAR = /[#{control}]/n
- ATOM_UNSAFE = /[#{Regexp.quote aspecial}#{control}#{lwsp}]/n
- PHRASE_UNSAFE = /[#{Regexp.quote aspecial}#{control}]/n
- TOKEN_UNSAFE = /[#{Regexp.quote tspecial}#{control}#{lwsp}]/n
+ ## reference: http://www.ownway.info/Blog/2011/09/-invalid-multibyte-escape.html
+ CONTROL_CHAR = Regexp.new("[#{control}]", nil, "n")
+ ATOM_UNSAFE = Regexp.new("[#{Regexp.quote aspecial}#{control}#{lwsp}]", nil, "n")
+ PHRASE_UNSAFE = Regexp.new("[#{Regexp.quote aspecial}#{control}]", nil, "n")
+ TOKEN_UNSAFE = Regexp.new("[#{Regexp.quote tspecial}#{control}#{lwsp}]", nil, "n")
# Returns true if the string supplied is free from characters not allowed as an ATOM
def atom_safe?( str )
diff -ru tmail-1.2.7.1.orig/./lib/tmail/vendor/rchardet-1.3/lib/rchardet/universaldetector.rb tmail-1.2.7.1/./lib/tmail/vendor/rchardet-1.3/lib/rchardet/universaldetector.rb
--- tmail-1.2.7.1.orig/./lib/tmail/vendor/rchardet-1.3/lib/rchardet/universaldetector.rb 2020-06-10 22:06:16.829961402 +0900
+++ tmail-1.2.7.1/./lib/tmail/vendor/rchardet-1.3/lib/rchardet/universaldetector.rb 2020-06-11 11:18:58.457912881 +0900
@@ -36,7 +36,7 @@
class UniversalDetector
attr_accessor :result
def initialize
- @_highBitDetector = /[\x80-\xFF]/
+ @_highBitDetector = Regex.new("[\x80-\xFF]")
@_escDetector = /(\033|\~\{)/
@_mEscCharSetProber = nil
@_mCharSetProbers = []
実際にNNTPサーバーにアクセスしてみる
オープンアクセスできるNetNewsサーバーが存在しているか分かりませんが、手元のINN 2.5.5にアクセスするコードは次のようになりました。
#!/usr/bin/ruby
require "bundler/setup"
Bundler.require
nntp = Net::NNTP.new
Net::NNTP.logger = Log4r::Logger.new("nntp")
nntp.host = "nntp.example.com"
nntp.port = 119
welcome = nntp.connect
if Net::NNTP::OKResponse === welcome
group_name = "misc.test"
group_response = nntp.process(Net::NNTP::Group.new(group_name))
listgroup_response = nntp.process(Net::NNTP::Listgroup.new(group_name))
listgroup_response.list.each { |article_id|
}
end
Subjectの文字化けへの対応
これはDockerとは関係ないので、上のセクションに追記しておきます。
tmailは格納したメッセージのヘッダーについて、いくつかのフィールドは固有のクラスを持っていますが、Subject:など固有の処理を行なわないものはUnstructuredHeaderクラスにその値が格納されています。
この中で、Decoder.decodeを呼び出しますが、この実装がNKF.nkfが"-mSj"オプション(MIME decode + Shift-JIS input encoding + ISO-2022-JP output encoding)で呼び出されるようになっています。
Decoder.decodeの実装を直してしまうと問題があるので、省略されている引数を利用し、output encodingをUTF8にしています。
またFrom:は、StructuredHeaderを継承しているAddressHeaderクラスを利用するため、また別の対応が必要です。nkfを呼び出す前のencodingとして"jes"(ISO-2022-JP,EUC-JP,Shift-JIS)のみを前提としたチェックルーチンが入っているので、この点については少しDecoderクラスの定義にも手を入れています。
diff -ur lib/ruby/2.5.0/gems/tmail-1.2.7.1.orig/lib/tmail/header.rb lib/ruby/2.5.0/gems/tmail-1.2.7.1/lib/tmail/header.rb
--- lib/ruby/2.5.0/gems/tmail-1.2.7.1.orig/lib/tmail/header.rb 2020-06-16 10:59:57.589086000 +0900
+++ lib/ruby/2.5.0/gems/tmail-1.2.7.1/lib/tmail/header.rb 2020-06-16 11:01:30.527304391 +0900
@@ -189,5 +189,5 @@
end
def parse
- @body = Decoder.decode(@body.gsub(/\n|\r\n|\r/, ''))
+ @body = Decoder.decode(@body.gsub(/\n|\r\n|\r/, ''), 'w')
end
def isempty?
diff -ur lib/ruby/2.7.0/gems/tmail-1.2.7.1.orig/lib/tmail/encode.rb lib/ruby/2.7.0/gems/tmail-1.2.7.1/lib/tmail/encode.rb
--- lib/ruby/2.7.0/gems/tmail-1.2.7.1.orig/lib/tmail/encode.rb 2020-06-18 14:32:37.352341212 +0900
+++ lib/ruby/2.7.0/gems/tmail-1.2.7.1/lib/tmail/encode.rb 2020-06-18 14:45:57.637057274 +0900
@@ -128,7 +128,7 @@
def initialize( dest, encoding = nil, eol = "\n" )
@f = StrategyInterface.create_dest(dest)
- @encoding = (/\A[ejs]/ === encoding) ? encoding[0,1] : nil
+ @encoding = (/\A[ejsw]/ === encoding) ? encoding[0,1] : nil
@eol = eol
end
diff -ur lib/ruby/2.7.0/gems/tmail-1.2.7.1.orig/lib/tmail/header.rb lib/ruby/2.7.0/gems/tmail-1.2.7.1/lib/tmail/header.rb
--- lib/ruby/2.7.0/gems/tmail-1.2.7.1.orig/lib/tmail/header.rb 2020-06-18 14:32:37.993666420 +0900
+++ lib/ruby/2.7.0/gems/tmail-1.2.7.1/lib/tmail/header.rb 2020-06-18 14:47:16.026796369 +0900
@@ -147,7 +147,7 @@
def body
ensure_parsed
- v = Decoder.new(s = '')
+ v = Decoder.new(s = '', "w")
do_accept v
v.terminate
s
@@ -225,7 +225,7 @@
rescue SyntaxError
if not save and mime_encoded? @body
save = @body
- @body = Decoder.decode(save)
+ @body = Decoder.decode(save, "w")
retry
elsif save
@body = save
Dockerのruby:2.7-alpineイメージや、Ubuntu 20.04 LTSなど、ruby-2.7な環境でpatchをあてる時には、あらかじめファイルの"2.5.0"を"2.7.0"に変換してから利用してください。
$ sed -e 's/2.5.0/2.7.0/g' tmail.diff | patch -p0
Dockerfileで遭遇したエラー
Dockerイメージは、ruby:2.7-alpineを利用しています。
エラーはいくつかあり、ruby-2.5では顕在化しなかった問題、root以外のユーザーで実行しようとした事に由来するものなどがありました。
とりあえずシンプルにするためにmulti stage buildなどを省いたDockerfileを以下に示します。
カレントディレクトリには、Gemfileやconfig.ruなどopenapi-generatorで作成したruby-sinatraのテンプレートコードが存在します。
FROM ruby:2.7-alpine as rubydev
RUN apk --no-cache add tzdata bash ca-certificates make gcc libc-dev linux-headers build-base patch
RUN mkdir /app
COPY . /app
WORKDIR /app
RUN cp /usr/local/include/ruby-2.7.0/ruby/defines.h /usr/local/include/ruby-2.7.0/defines.h
RUN bundle config path lib
RUN bundle install
ENV SINATRA_PORT 8080
EXPOSE $SINATRA_PORT
ADD run.sh /run.sh
RUN chmod +x /run.sh
RUN addgroup sinatra
RUN adduser -S -G sinatra sinatra
# RUN cp -r /root/.bundle /home/sinatra/.bundle
USER sinatra
ENTRYPOINT ["/run.sh"]
ruby-2.7でtmailのnative extensionがコンパイルできない問題
defines.hがコンパイルできないとエラーになります。
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.
...
current directory: /app/lib/ruby/2.7.0/gems/tmail-1.2.7.1/ext/tmailscanner/tmail
make "DESTDIR="
make: *** No rule to make target '/usr/local/include/ruby-2.7.0/defines.h',
needed by 'tmailscanner.o'. Stop.
...
期待する場所にdefines.hが存在しないため、cpして対応しました。
RUN cp /usr/local/include/ruby-2.7.0/ruby/defines.h \
/usr/local/include/ruby-2.7.0/defines.h
Ubuntu 18.04 + ruby-2.5 (deb package)な環境では、Makefileの最後に明示的にruby/defines.hを指定していましたが、Dockerfileの中でtmailのMakefileを確認した限りでは、-Iフラグに/usr/local/include/ruby-2.7.0までが指定されているだけのようでした。
tmailscanner.o: tmailscanner.c $(hdrdir)/ruby/ruby.h $(arch_hdrdir)/ruby/config.h $(hdrdir)/ruby/defines.h Makefile
ruby:2.7-alpineの環境では、defines.hはruby/defines.hとなっているべきところが異なっていました。
tmailscanner.o: tmailscanner.c $(hdrdir)/ruby.h $(arch_hdrdir)/ruby/config.h $(hdrdir)/defines.h Makefile
原因はmkmf.rbにあって、dependファイルを書き出す前処理で、$(hdrdir)/defines.h
となっている行を、$(hdrdir)ruby/defines.h
へと変更する処理がruby-2.7のmkmf.rbでは削除されているからでした。
## ruby-2.5のmkmf.rb
depend.each_line do |line|
line.gsub!(/\.o\b/, ".#{$OBJEXT}")
line.gsub!(/\{\$\(VPATH\)\}/, "") unless $nmake
line.gsub!(/\$\((?:hdr|top)dir\)\/config.h/, $config_h)
line.gsub!(%r"\$\(hdrdir\)/(?!ruby(?![^:;/\s]))(?=[-\w]+\.h)", '\&ruby/')
if $nmake && /\A\s*\$\(RM|COPY\)/ =~ line
## ruby-2.7のmkmf.rb
depend.each_line do |line|
line.gsub!(/\.o\b/, ".#{$OBJEXT}")
line.gsub!(/\{\$\(VPATH\)\}/, "") unless $nmake
line.gsub!(/\$\((?:hdr|top)dir\)\/config.h/, $config_h)
if $nmake && /\A\s*\$\(RM|COPY\)/
https://github.com/ruby/ruby/commit/3d1c86a26f0c96a9c1d0247b968aa96e6f3c30bb#diff-7aa560cb6196deeb96779cd175e8e589 でコミットされた変更のようです。
ruby.hの配置場所を修正したかったようですが、おそらく該当行を削除するのではなく、この下に”ruby/ruby.h"を"ruby.h"に書き換えるようなgsub!行を追加するべきだったのかなと思います。
RubyのIssue Tracking Systemには、https://bugs.ruby-lang.org/issues/16490 として登録されていますが、検討は進んでいないようですね。
Dockerで利用するような場合、mkmf.rbにpatchを当てるよりは、defines.hの場所を変更する方法で対応する回避策を取る他にないですね…。
rackupが見つからないエラーへの対応
ruby-2.5では、$ bundle install --path lib
を利用していましたが、deprecatedと警告がでたので$ bundle config path lib
を利用するように変更しています。
このため、~/.bundle/configファイルの内容に依存するため、USER sinatra
によって実行時のユーザーを変更したため、~/.bundle/configファイルが存在しないことでエラーに遭遇しました。
$ make docker-run
sudo docker run -it --rm -p 8080:8080 --name nntp-reader nntp-reader:latest
bundler: command not found: rackup
Install missing gem executables with `bundle install`
Makefile:43: recipe for target 'docker-run' failed
make: *** [docker-run] Error 127
以下のような対応がまず考えられます。
#!/bin/bash
bundle config path lib ## 追加したワークアラウンド
export SINATRA_PORT=${SINATRA_PORT:-8080}
bundle exec rackup --host 0.0.0.0 --port $SINATRA_PORT
今回採用したのは次の方法でした。
RUN cp -r /root/.bundle /home/sinatra/.bundle
さいごに
NNTPは学生時代に始めて触ったプロトコルの一つで、RFCを読みながら自分用のRubyライブラリを実装したりしていました。
現代ではオープンなプロトコルよりもクローズドでも便利そうなサービスが流行るようになってしまい廃れていくのが残念でなりませんが、もしまだ役に立つようであれば幸いです。
以上