# encoding: UTF-8
# == Schema Information
#
# Table name: foi_attachments
#
# id :integer not null, primary key
# content_type :text
# filename :text
# charset :text
# display_size :text
# url_part_number :integer
# within_rfc822_subject :text
# incoming_message_id :integer
# hexdigest :string(32)
#
# models/foi_attachment.rb:
# An attachment to an email (IncomingMessage)
#
# Copyright (c) 2007 UK Citizens Online Democracy. All rights reserved.
# Email: hello@mysociety.org; WWW: http://www.mysociety.org/
# This is the type which is used to send data about attachments to the view
require 'digest'
class FoiAttachment < ActiveRecord::Base
belongs_to :incoming_message
validates_presence_of :content_type
validates_presence_of :filename
validates_presence_of :display_size
before_validation :ensure_filename!, :only => [:filename]
before_destroy :delete_cached_file!
BODY_MAX_TRIES = 3
BODY_MAX_DELAY = 5
def directory
base_dir = File.expand_path(File.join(File.dirname(__FILE__), "../../cache", "attachments_#{Rails.env}"))
return File.join(base_dir, self.hexdigest[0..2])
end
def filepath
File.join(self.directory, self.hexdigest)
end
def delete_cached_file!
begin
@cached_body = nil
File.delete(self.filepath)
rescue
end
end
def body=(d)
self.hexdigest = Digest::MD5.hexdigest(d)
if !File.exists?(self.directory)
FileUtils.mkdir_p self.directory
end
File.open(self.filepath, "wb") { |file|
file.write d
}
update_display_size!
@cached_body = d
end
def body
if @cached_body.nil?
tries = 0
delay = 1
begin
binary_data = File.open(self.filepath, "rb" ){ |file| file.read }
if self.content_type =~ /^text/
@cached_body = convert_string_to_utf8_or_binary(binary_data, 'UTF-8')
else
@cached_body = binary_data
end
rescue Errno::ENOENT
# we've lost our cached attachments for some reason. Reparse them.
if tries > BODY_MAX_TRIES
raise
else
sleep delay
end
tries += 1
delay *= 2
delay = BODY_MAX_DELAY if delay > BODY_MAX_DELAY
force = true
self.incoming_message.parse_raw_email!(force)
retry
end
end
return @cached_body
end
# List of DSN codes taken from RFC 3463
# http://tools.ietf.org/html/rfc3463
DsnToMessage = {
'X.1.0' => 'Other address status',
'X.1.1' => 'Bad destination mailbox address',
'X.1.2' => 'Bad destination system address',
'X.1.3' => 'Bad destination mailbox address syntax',
'X.1.4' => 'Destination mailbox address ambiguous',
'X.1.5' => 'Destination mailbox address valid',
'X.1.6' => 'Mailbox has moved',
'X.1.7' => 'Bad sender\'s mailbox address syntax',
'X.1.8' => 'Bad sender\'s system address',
'X.2.0' => 'Other or undefined mailbox status',
'X.2.1' => 'Mailbox disabled, not accepting messages',
'X.2.2' => 'Mailbox full',
'X.2.3' => 'Message length exceeds administrative limit.',
'X.2.4' => 'Mailing list expansion problem',
'X.3.0' => 'Other or undefined mail system status',
'X.3.1' => 'Mail system full',
'X.3.2' => 'System not accepting network messages',
'X.3.3' => 'System not capable of selected features',
'X.3.4' => 'Message too big for system',
'X.4.0' => 'Other or undefined network or routing status',
'X.4.1' => 'No answer from host',
'X.4.2' => 'Bad connection',
'X.4.3' => 'Routing server failure',
'X.4.4' => 'Unable to route',
'X.4.5' => 'Network congestion',
'X.4.6' => 'Routing loop detected',
'X.4.7' => 'Delivery time expired',
'X.5.0' => 'Other or undefined protocol status',
'X.5.1' => 'Invalid command',
'X.5.2' => 'Syntax error',
'X.5.3' => 'Too many recipients',
'X.5.4' => 'Invalid command arguments',
'X.5.5' => 'Wrong protocol version',
'X.6.0' => 'Other or undefined media error',
'X.6.1' => 'Media not supported',
'X.6.2' => 'Conversion required and prohibited',
'X.6.3' => 'Conversion required but not supported',
'X.6.4' => 'Conversion with loss performed',
'X.6.5' => 'Conversion failed',
'X.7.0' => 'Other or undefined security status',
'X.7.1' => 'Delivery not authorized, message refused',
'X.7.2' => 'Mailing list expansion prohibited',
'X.7.3' => 'Security conversion required but not possible',
'X.7.4' => 'Security features not supported',
'X.7.5' => 'Cryptographic failure',
'X.7.6' => 'Cryptographic algorithm not supported',
'X.7.7' => 'Message integrity failure'
}
# Returns HTML, of extra comment to put by attachment
def extra_note
# For delivery status notification attachments, extract the status and
# look up what it means in the DSN table.
if @content_type == 'message/delivery-status'
if !@body.match(/Status:\s+([0-9]+\.([0-9]+\.[0-9]+))\s+/)
return ""
end
dsn = $1
dsn_part = 'X.' + $2
dsn_message = ""
if DsnToMessage.include?(dsn_part)
dsn_message = " (" + DsnToMessage[dsn_part] + ")"
end
return "
DSN: " + dsn + dsn_message + ""
end
return ""
end
# Called by controller so old filenames still work
def old_display_filename
filename = self.filename
# Convert weird spaces (e.g. \n) to normal ones
filename = filename.gsub(/\s/, " ")
# Remove slashes, they mess with URLs
filename = filename.gsub(/\//, "-")
return filename
end
# TODO: changing this will break existing URLs, so have a care - maybe
# make another old_display_filename see above
def display_filename
filename = self.filename
if !self.incoming_message.nil?
self.incoming_message.info_request.apply_censor_rules_to_text!(filename)
end
# Sometimes filenames have e.g. %20 in - no point butchering that
# (without unescaping it, this would remove the % and leave 20s in there)
filename = CGI.unescape(filename)
# Remove weird spaces
filename = filename.gsub(/\s+/, " ")
# Remove non-alphabetic characters
filename = filename.gsub(/[^A-Za-z0-9.]/, " ")
# Remove spaces near dots
filename = filename.gsub(/\s*\.\s*/, ".")
# Compress adjacent spaces down to a single one
filename = filename.gsub(/\s+/, " ")
filename = filename.strip
return filename
end
def ensure_filename!
if self.filename.blank?
calc_ext = AlaveteliFileTypes.mimetype_to_extension(self.content_type)
if !calc_ext
calc_ext = "bin"
end
if !self.within_rfc822_subject.nil?
computed = self.within_rfc822_subject + "." + calc_ext
else
computed = "attachment." + calc_ext
end
self.filename = computed
end
end
def filename=(filename)
calc_ext = AlaveteliFileTypes.mimetype_to_extension(self.content_type)
# Put right extension on if missing
if !filename.nil? && !filename.match(/\.#{calc_ext}$/) && calc_ext
computed = filename + "." + calc_ext
else
computed = filename
end
write_attribute('filename', computed)
end
# Size to show next to the download link for the attachment
def update_display_size!
s = self.body.size
if s > 1024 * 1024
self.display_size = sprintf("%.1f", s.to_f / 1024 / 1024) + 'M'
else
self.display_size = (s / 1024).to_s + 'K'
end
end
# Whether this type can be shown in the Google Docs Viewer.
# The full list of supported types can be found at
# https://docs.google.com/support/bin/answer.py?hl=en&answer=1189935
def has_google_docs_viewer?
return !! {
"application/pdf" => true, # .pdf
"image/tiff" => true, # .tiff
"application/vnd.ms-word" => true, # .doc
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" => true, # .docx
"application/vnd.ms-powerpoint" => true, # .ppt
"application/vnd.openxmlformats-officedocument.presentationml.presentation" => true, # .pptx
"application/vnd.ms-excel" => true, # .xls
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" => true, # .xlsx
} [self.content_type]
end
# Whether this type has a "View as HTML"
def has_body_as_html?
return (
!!{
"text/plain" => true,
"application/rtf" => true,
}[self.content_type] or
self.has_google_docs_viewer?
)
end
# Name of type of attachment type - only valid for things that has_body_as_html?
def name_of_content_type
return {
"text/plain" => "Text file",
'application/rtf' => "RTF file",
'application/pdf' => "PDF file",
'image/tiff' => "TIFF image",
'application/vnd.ms-word' => "Word document",
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => "Word document",
'application/vnd.ms-powerpoint' => "PowerPoint presentation",
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => "PowerPoint presentation",
'application/vnd.ms-excel' => "Excel spreadsheet",
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => "Excel spreadsheet",
}[self.content_type]
end
# For "View as HTML" of attachment
def body_as_html(dir, opts = {})
attachment_url = opts.fetch(:attachment_url, nil)
to_html_opts = opts.merge(:tmpdir => dir, :attachment_url => attachment_url)
AttachmentToHTML.to_html(self, to_html_opts)
end
end