tilde.news/script/mail_new_activity.rb

239 lines
7.3 KiB
Ruby
Executable File

#!/usr/bin/env ruby
APP_PATH = File.expand_path("../../config/application", __FILE__)
require File.expand_path("../../config/boot", __FILE__)
require APP_PATH
Rails.application.require_environment!
class String
def quoted_printable(encoded_word = false)
string = [self].pack("M")
return string unless encoded_word
q_encode_word = ->(w) { "=?UTF-8?Q?#{w}?=" }
string
# Undo linebreaks from #pack("M") because we'll be adding characters
.gsub("=\n", "")
# Question marks are delimiters in q-encoding so must be escaped
.gsub("?", "=3F")
# Spaces are insignificant in q-encoding so must be escaped
.gsub(/\s+/, " _")
# Take each space-separated word, then q-encode
.split(" ").map(&q_encode_word)
# Recombine words then word wrap at 75 characters
.join(" ").word_wrap(75)
# Compose final string, folding headers per rfc 2822 section 2.2.3
.lines.join("\t")
end
# like ActionView::Helpers::TextHelper but preserve > and indentation when
# wrapping lines
def word_wrap(len)
split("\n").collect do |line|
if line.length <= len
line
elsif (m = line.match(/^(> ?| +)(.*)/))
ind = m[1]
if len - ind.length <= 0
ind = " "
end
m[2].gsub(/(.{1,#{len - ind.length}})(\s+|$)/, "#{ind}\\1\n").strip
else
line.gsub(/(.{1,#{len}})(\s+|$)/, "\\1\n").strip
end
end * "\n"
end
end
def story_subject(story, prefix = "")
ss = "#{prefix}#{story.title}"
story.tags.sort_by(&:tag).each do |t|
ss << " [#{t.tag}]"
end
ss.quoted_printable(true)
end
if __FILE__ == $PROGRAM_NAME
EMAIL_WIDTH = 72
LAST_STORY_KEY = "mailing:last_story_id".freeze
LAST_COMMENT_KEY = "mailing:last_comment_id".freeze
mailing_list_users = User.where("mailing_list_mode > 0").select(&:is_active?)
last_story_id = (Keystore.value_for(LAST_STORY_KEY) || Story.last && Story.last.id).to_i
# Paranoia: only search back three days so that if last_story_id is oddly low we don't start
# sending every story from the beginning of time, or if mailing list mode breaks for more than a
# few days we won't bury them in email.
Story
.where("id > ? AND is_deleted = ? AND created_at >= ?", last_story_id, false, 3.days.ago)
.order(:id)
.each do |s|
StoryText.fill_cache!(s)
mailing_list_users.each do |u|
if (s.tags.map(&:id) & u.tag_filters.map(&:tag_id)).any?
# story has tags this user has filtered out
next
end
if s.is_hidden_by_user?(u)
# user has hidden this story
next
end
list = "#{Rails.application.shortname}-#{u.mailing_list_token}@#{Rails.application.domain}"
IO.popen(
[{}, "/usr/sbin/sendmail", "-i", "-f", "nobody@#{Rails.application.domain}", u.email],
"w"
) do |mail|
mail.puts "From: #{s.user.username} <#{s.user.username}@#{Rails.application.domain}>"
mail.puts "X-Is-Author: #{s.user_is_author?}"
mail.puts "Reply-To: #{list}"
mail.puts "To: #{list}"
mail.puts "X-BeenThere: #{list}"
mail.puts "List-Id: #{Rails.application.name} <#{list}>"
mail.puts "List-Unsubscribe: <#{Rails.application.root_url}settings>"
mail.puts "Precedence: list"
mail.puts "MIME-Version: 1.0"
mail.puts "Content-Type: text/plain; charset=\"utf-8\""
mail.puts "Content-Transfer-Encoding: quoted-printable"
mail.puts "Message-ID: <#{s.mailing_list_message_id}>"
mail.puts "Date: " << s.created_at.strftime("%a, %d %b %Y %H:%M:%S %z")
mail.puts "Subject: " << story_subject(s)
mail.puts ""
body = []
if s.description.present?
body.push s.description.to_s.word_wrap(EMAIL_WIDTH)
end
if s.url.present?
if s.description.present?
body.push ""
end
body.push "Via: #{s.url}"
StoryText.cached?(s) do |text|
body.push ""
body.push text.to_s.word_wrap(EMAIL_WIDTH)
end
end
body.push ""
body.push "-- "
body.push "Vote: #{s.short_id_url}"
mail.puts body.join("\n").quoted_printable
end
end
last_story_id = s.id
end
Keystore.put(LAST_STORY_KEY, last_story_id)
# repeat for comments
last_comment_id = (Keystore.value_for(LAST_COMMENT_KEY) || Comment.last && Comment.last.id).to_i
# Paranoia: only search back three days so that if last_comment_id is oddly low we don't start
# sending every comment from the beginning of time, or if mailing list mode breaks for more than a
# few days we won't bury them in email.
Comment.where(
"id > ? AND (is_deleted = ? AND is_moderated = ?) AND created_at >= ?",
last_comment_id,
false,
false,
3.days.ago
).order(:id).each do |c|
# allow some time for newer comments to be edited before sending them out
if (Time.current - (c.updated_at || c.created_at)) < 2.minutes
break
end
mailing_list_users.each do |u|
if u.mailing_list_mode == 2
# stories only
next
end
if (c.story.tags.map(&:id) & u.tag_filters.map(&:tag_id)).any?
# story has tags this user has filtered out
next
end
if c.story.is_hidden_by_user?(u)
# user has hidden this story
next
end
domain = Rails.application.domain
list = "#{Rails.application.shortname}-#{u.mailing_list_token}@#{Rails.application.domain}"
IO.popen([{}, "/usr/sbin/sendmail", "-i", "-f", "nobody@#{domain}", u.email], "w") do |mail|
from = "From: \"#{c.user.username}"
if c.hat
from << " (#{c.hat.hat})"
end
from << "\" <#{c.user.username}@#{domain}>"
mail.puts from
mail.puts "Reply-To: #{list}"
mail.puts "To: #{list}"
mail.puts "List-Id: #{Rails.application.name} <#{list}>"
mail.puts "List-Unsubscribe: <#{Rails.application.root_url}settings>"
mail.puts "Precedence: list"
mail.puts "MIME-Version: 1.0"
mail.puts "Content-Type: text/plain; charset=\"utf-8\""
mail.puts "Content-Transfer-Encoding: quoted-printable"
mail.puts "Message-ID: <#{c.mailing_list_message_id}>"
if c.parent_comment_id
mail.puts "In-Reply-To: <#{c.parent_comment.mailing_list_message_id}>"
else
mail.puts "In-Reply-To: <#{c.story.mailing_list_message_id}>"
end
refs = ([c.story.mailing_list_message_id] +
c.parents.map(&:mailing_list_message_id))
.map { |r| "<#{r}>" }
mail.puts "References: #{refs.join(" ")}"
mail.puts "Date: " << c.created_at.strftime("%a, %d %b %Y %H:%M:%S %z")
mail.puts "Subject: " << story_subject(c.story, "Re: ")
mail.puts ""
body = []
# if the comment has hard line breaks at <80, it likely came from an
# email, so don't re-wrap it at something shorter
com = c.comment.to_s
com_lines = com.split("\n")
if com_lines.length > 1 && com_lines.first.length < 80
body.push com.word_wrap(80)
else
body.push com.word_wrap(EMAIL_WIDTH)
end
body.push ""
body.push "-- "
body.push "Vote: #{c.short_id_url}"
mail.puts body.join("\n").quoted_printable
end
end
last_comment_id = c.id
end
Keystore.put(LAST_COMMENT_KEY, last_comment_id)
end