module Schleuder::Filters

Public Class Methods

fix_exchange_messages(list, mail) click to toggle source

Outlook / Hotmail seems to dismantle multipart/encrypted messages and put them again together as multipart/mixed, which is wrong and makes it problematic to correctly detect the message as a valid pgp/mime-mail. Here we fix the mail to be a proper pgp/mime aka. multipart/encrypted message, so further processing will detect it properly. This problem seems to be in fact related to the use of Microsoft Exchange. Accordingly, check if the headers contain ‘X-MS-Exchange’. See #211, #246, #331 and #333 for background.

# File lib/schleuder/filters/pre_decryption/40_fix_exchange_messages.rb, line 12
def self.fix_exchange_messages(list, mail)
  if mail.header_fields.any?{|f| f.name =~ /^X-MS-Exchange-/i } &&
      !mail[:content_type].blank? &&
      mail[:content_type].content_type == 'multipart/mixed' && mail.parts.size > 2 &&
      mail.parts[0][:content_type].content_type == 'text/plain' &&
      mail.parts[0].body.to_s.blank? &&
      mail.parts[1][:content_type].content_type == 'application/pgp-encrypted' &&
      mail.parts[2][:content_type].content_type == 'application/octet-stream'
    mail.parts.delete_at(0)
    mail.content_type = [:multipart, :encrypted, {protocol: 'application/pgp-encrypted', boundary: mail.boundary}]
  end
end
forward_all_incoming_to_admins(list, mail) click to toggle source
# File lib/schleuder/filters/pre_decryption/20_forward_all_incoming_to_admins.rb, line 4
def self.forward_all_incoming_to_admins(list, mail)
  if list.forward_all_incoming_to_admins
    list.logger.notify_admin I18n.t(:forward_all_incoming_to_admins), mail.original_message, I18n.t('incoming_message')
  end
end
forward_bounce_to_admins(list, mail) click to toggle source
# File lib/schleuder/filters/pre_decryption/10_forward_bounce_to_admins.rb, line 3
def self.forward_bounce_to_admins(list, mail)
  if mail.automated_message?
    list.logger.info 'Forwarding automated message to admins'
    list.logger.notify_admin I18n.t(:forward_automated_message_to_admins), mail.original_message, I18n.t('automated_message_subject')
    exit
  end
end
forward_to_owner(list, mail) click to toggle source
# File lib/schleuder/filters/post_decryption/30_forward_to_owner.rb, line 3
def self.forward_to_owner(list, mail)
  return if ! mail.to_owner?

  list.logger.debug 'Forwarding addressed to -owner'
  mail.add_pseudoheader(:note, I18n.t(:owner_forward_prefix))
  cleanmail = mail.clean_copy(true)
  list.admins.each do |admin|
    list.logger.debug "Forwarding message to #{admin}"
    admin.send_mail(cleanmail)
  end
  exit
end
key_auto_import_from_attachments(list, mail) click to toggle source
# File lib/schleuder/filters/post_decryption/35_key_auto_import_from_attachments.rb, line 3
def self.key_auto_import_from_attachments(list, mail)
  # Don't run if not enabled.
  return if ! list.key_auto_import_from_email

  imported_fingerprints = EmailKeyImporter.import_from_attachments(list, mail)
  if imported_fingerprints.size > 0
    # If the message's signature could not be validated before, re-run the
    # validation, because after having imported new or updated keys the
    # validation now might work.
    if mail.signature.present? && ! mail.signature.valid?
      # Re-validate the signature validation, now that a new key was
      # imported that might be the previously unknown signing key.
      mail.repeat_validation!
    end
  end
end
key_auto_import_from_autocrypt_header(list, mail) click to toggle source
# File lib/schleuder/filters/pre_decryption/60_key_auto_import_from_autocrypt_header.rb, line 3
def self.key_auto_import_from_autocrypt_header(list, mail)
  if list.key_auto_import_from_email
    EmailKeyImporter.import_from_autocrypt_header(list, mail)
  end
end
max_message_size(list, mail) click to toggle source
# File lib/schleuder/filters/post_decryption/20_max_message_size.rb, line 4
def self.max_message_size(list, mail)
  if (mail.raw_source.size / 1024) > list.max_message_size_kb
    list.logger.info 'Rejecting mail as too big'
    return Errors::MessageTooBig.new(list)
  end
end
receive_admin_only(list, mail) click to toggle source
# File lib/schleuder/filters/post_decryption/40_receive_admin_only.rb, line 3
def self.receive_admin_only(list, mail)
  if list.receive_admin_only? && ( ! mail.was_validly_signed? || ! mail.signer.admin? )
    list.logger.info 'Rejecting mail as not from admin.'
    return Errors::MessageNotFromAdmin.new
  end
end
receive_authenticated_only(list, mail) click to toggle source
# File lib/schleuder/filters/post_decryption/50_receive_authenticated_only.rb, line 3
def self.receive_authenticated_only(list, mail)
  if list.receive_authenticated_only? && ( ! mail.was_encrypted? || ! mail.was_validly_signed? )
    list.logger.info 'Rejecting mail as unauthenticated'
    return Errors::MessageUnauthenticated.new
  end
end
receive_encrypted_only(list, mail) click to toggle source
# File lib/schleuder/filters/post_decryption/70_receive_encrypted_only.rb, line 3
def self.receive_encrypted_only(list, mail)
  if list.receive_encrypted_only? && ! mail.was_encrypted?
    list.logger.info 'Rejecting mail as unencrypted'
    return Errors::MessageUnencrypted.new
  end
end
receive_from_subscribed_emailaddresses_only(list, mail) click to toggle source
# File lib/schleuder/filters/post_decryption/80_receive_from_subscribed_emailaddresses_only.rb, line 3
def self.receive_from_subscribed_emailaddresses_only(list, mail)
  if list.receive_from_subscribed_emailaddresses_only? && list.subscriptions.where(email: mail.from.first.downcase).blank?
    list.logger.info 'Rejecting mail as not from subscribed address.'
    return Errors::MessageSenderNotSubscribed.new
  end
end
receive_signed_only(list, mail) click to toggle source
# File lib/schleuder/filters/post_decryption/60_receive_signed_only.rb, line 3
def self.receive_signed_only(list, mail)
  if list.receive_signed_only? && ! mail.was_validly_signed?
    list.logger.info 'Rejecting mail as unsigned'
    return Errors::MessageUnsigned.new
  end
end
recursively_strip_html_from_alternative_if_keywords_present(list, mail) click to toggle source
# File lib/schleuder/filters/post_decryption/90_strip_html_from_alternative_if_keywords_present.rb, line 15
def self.recursively_strip_html_from_alternative_if_keywords_present(list, mail)
  if mail[:content_type].blank? then return false end
  content_type = mail[:content_type].content_type

  # The multipart/alternative could hide inside an arbitrary number of
  # levels of multipart/mixed encapsulation.
  # see also: https://www.rfc-editor.org/rfc/rfc2046#section-5.1.3
  if content_type == 'multipart/mixed'
    mail.parts.each do |part|
      self.recursively_strip_html_from_alternative_if_keywords_present(list, part)
    end
    return false
  end

  # inside the mutlipart/mixed, we only care about multipart/mixed and
  # mutlipart/alternative
  if content_type != 'multipart/alternative' then return false end

  # Inside multipart/alternative, there could be a text/html-part, or there
  # could be a multipart/related-part which contains the text/html-part.
  # Everything inside the multipart/alternative that is not text/plain
  # should be deleted, since it will contain keywords and we only strip
  # keywords from text/plain-parts.
  Schleuder.logger.debug 'Stripping html-part from multipart/alternative-message because it contains keywords'
  mail.parts.delete_if do |part|
    content_type = part[:content_type].content_type
    content_type != 'text/plain'
  end

  # NOTE: We could instead unencapsulate it.
  mail.content_type = 'multipart/mixed'

  mail.add_pseudoheader(:note, I18n.t('pseudoheaders.stripped_html_from_multialt_with_keywords'))
end
request(list, mail) click to toggle source
# File lib/schleuder/filters/post_decryption/10_request.rb, line 3
def self.request(list, mail)
  return if ! mail.request?

  list.logger.debug 'Request-message'

  if ! mail.was_encrypted? || ! mail.was_validly_signed?
    list.logger.debug 'Error: Message was not encrypted and validly signed'
    return Errors::MessageUnauthenticated.new
  end

  if mail.keywords.empty?
    output = I18n.t(:no_keywords_error)
  else
    output = KeywordHandlersRunner.run(type: :request, list: list, mail: mail)
    output = output.flatten.map(&:presence).compact
    if output.blank?
      output = I18n.t(:no_output_result)
    end
  end
  mail.reply_to_signer(output)
  exit
end
send_key(list, mail) click to toggle source
# File lib/schleuder/filters/pre_decryption/30_send_key.rb, line 3
def self.send_key(list, mail)
  return if ! mail.sendkey_request?

  list.logger.debug 'Sending public key as reply.'

  out = mail.reply
  out.from = list.email
  # We're not sending to a subscribed address, so we need to specify a envelope-sender manually.
  out.sender = list.bounce_address
  out.body = I18n.t(:list_public_key_attached)
  out.attach_list_key!(list)
  # TODO: find out why the gpg-module puts all the headers into the first mime-part, too
  out.gpg list.gpg_sign_options
  out.deliver
  exit
end
strip_html_from_alternative(list, mail) click to toggle source
# File lib/schleuder/filters/pre_decryption/50_strip_html_from_alternative.rb, line 4
def self.strip_html_from_alternative(list, mail)
  if mail[:content_type].blank? ||
        mail[:content_type].content_type != 'multipart/alternative' ||
        ! mail.to_s.include?('BEGIN PGP ')
    return false
  end

  Schleuder.logger.debug 'Stripping html-part from multipart/alternative-message'
  mail.parts.delete_if do |part|
    part[:content_type].content_type == 'text/html'
  end
  mail.content_type = 'multipart/mixed'
  mail.add_pseudoheader(:note, I18n.t('pseudoheaders.stripped_html_from_multialt'))
end
strip_html_from_alternative_if_keywords_present(list, mail) click to toggle source

If keywords are present, recurse into arbitrary levels of multipart/mixed encapsulation. If multipart/alternative is found, remove all sub-parts but the text/plain part (assuming that every multipart/alternative contains exactly one text/plain). Change the content_type from mutlipart/alternative to mutlipart/mixed.

# File lib/schleuder/filters/post_decryption/90_strip_html_from_alternative_if_keywords_present.rb, line 9
def self.strip_html_from_alternative_if_keywords_present(list, mail)
  # Only strip the text/html-part if keywords are present
  if mail.keywords.blank? then return false end
  return self.recursively_strip_html_from_alternative_if_keywords_present(list, mail)
end