| Class | MMS2R::Media |
| In: |
lib/mms2r.rb
lib/mms2r/media.rb lib/mms2r/media/sprint.rb |
| Parent: | Object |
| VERSION | = | '2.4.0' | MMS2R library version | |
| MULTIPARTS_TO_SPLIT | = | [ 'multipart/related', 'multipart/alternative', 'multipart/mixed', 'multipart/appledouble' ] | Various multi-parts that are bundled into mail |
| carrier | [R] | Carrier is the domain name of the carrier. If the carrier is not known the carrier will be set to ‘mms2r.media‘ |
| [R] | TMail object that the media files were derived from. | |
| media | [R] | media returns the hash of media. The media hash is keyed by mime-type such as ‘text/plain’ and the value mapped to the key is an array of media that are of that type. |
| media_dir | [R] | Base working dir where media for a unique mms message are dropped |
Get the directory where conf files are stored.
# File lib/mms2r/media.rb, line 619 def self.conf_dir @@conf_dir ||= File.join(File.dirname(__FILE__), '..', '..', 'conf') end
Set the directory where conf files are stored.
# File lib/mms2r/media.rb, line 626 def self.conf_dir=(d) @@conf_dir=d end
Factory method that creates MMS2R::Media products based on the domain name of the carrier from which the MMS originated. mail is a TMail object.
# File lib/mms2r/media.rb, line 156 def self.create(mail) d = lambda{ ['mms2r.media', MMS2R::Media] } #sets a default to detect from_domain = self.domain(mail) processor = MMS2R::CARRIERS.detect(d) do |domain, klass| return klass, domain if from_domain == domain end [MMS2R::Media, from_domain] end
Returns a default file extension based on a content type
# File lib/mms2r/media.rb, line 641 def self.default_ext(content_type) if MMS2R::EXT[content_type] MMS2R::EXT[content_type] elsif content_type content_type.split('/').last end end
Determine if return-path or from is going to be used to desiginate the origin carrier. If the domain in the From header is listed in conf/from.yaml then that is the carrier domain. Else if there is a Return-Path header its address’s domain is the carrier doamin, else use From header’s address domain.
# File lib/mms2r/media.rb, line 172 def self.domain(mail) return_path = case when mail.header['return-path'] matched = /^<.+@([^@]+)>$/.match(mail.header['return-path'].to_s.strip) matched ? matched[1] : '' else '' end from_domain = case when mail.from && mail.from.first mail.from.first.split('@').last else '' end f = File.join(self.conf_dir(), "from.yml") from = YAML::load_file(f) rescue {} ret = case when from.include?(from_domain) from_domain when return_path.any? return_path else from_domain end ret end
Joins the generic mms2r configuration with the carrier specific configuration.
# File lib/mms2r/media.rb, line 653 def self.initialize_config(c) f = File.join(self.conf_dir(), "mms2r_media.yml") conf = YAML::load_file(f) rescue {} conf['ignore'] ||= {} unless conf['ignore'] conf['transform'] = {} unless conf['transform'] conf['number'] = [] unless conf['number'] return conf unless c kinds = ['ignore', 'transform'] kinds.each do |kind| if c[kind] c[kind].each do |type,array| conf[kind][type] = [] unless conf[kind][type] conf[kind][type] += array end end end conf['number'] = c['number'] if c['number'] conf end
# File lib/mms2r/media.rb, line 110 def new(mail, opts = {}) klass, carrier = MMS2R::Media.create(mail) opts[:domain] = carrier klass.orig_new(mail, opts) end
Initialize a new MMS2R::Media comprised of a mail.
Specify options to initialize with: :logger => some_logger for logging :process => :lazy, for non-greedy processing upon initialization
process will have to be called explicitly if the lazy process option is chosen.
# File lib/mms2r/media.rb, line 213 def initialize(mail, opts={}) @mail = mail @logger = opts[:logger] log("#{self.class} created", :info) @carrier = opts[:domain] @dir_count = 0 @media_dir = File.join(self.tmp_dir(), "#{self.safe_message_id(@mail.message_id)}_#{UUIDTools::UUID.random_create}") @media = {} @was_processed = false @number = nil @subject = nil @body = nil @default_media = nil @default_text = nil f = File.join(self.conf_dir(), "aliases.yml") @aliases = YAML::load_file(f) rescue {} conf = "#{@aliases[@carrier] || @carrier}.yml" f = File.join(self.conf_dir(), conf) c = YAML::load_file(f) rescue {} @config = self.class.initialize_config(c) lazy = (opts[:process] == :lazy) rescue false self.process() unless lazy end
Helper to create a safe directory path element based on the mail message id.
# File lib/mms2r/media.rb, line 634 def self.safe_message_id(mid) mid.nil? ? "#{Time.now.to_i}" : mid.gsub(/\$|<|>|@|\./, "") end
Get the temporary directory where media files are written to.
# File lib/mms2r/media.rb, line 606 def self.tmp_dir @@tmp_dir ||= File.join(Dir.tmpdir, (ENV['USER'].nil? ? '':ENV['USER']), 'mms2r') end
Set the temporary directory where media files are written to.
# File lib/mms2r/media.rb, line 612 def self.tmp_dir=(d) @@tmp_dir=d end
Helper to add a file to the media hash.
# File lib/mms2r/media.rb, line 489 def add_file(type, file) media[type] = [] unless media[type] media[type] << file end
Convenience method that returns a string including all the text of the first text/plain file found. Returns empty string if no body text is found.
# File lib/mms2r/media.rb, line 281 def body text_file = default_text @body = text_file ? IO.readlines(text_file.path).join.strip : "" @body end
convenience accessor for self.class.conf_dir
# File lib/mms2r/media.rb, line 683 def conf_dir self.class.conf_dir end
convenience accessor for self.class.default_ext
# File lib/mms2r/media.rb, line 697 def default_ext(type) self.class.default_ext(type) end
Returns a File with the most likely candidate for the user-submitted media. Given that most MMS messages only have one file attached, this method will try to return that file. Singleton methods are added to the File object so it can be used in place of a CGI upload (local_path, original_filename, size, and content_type) such as in conjunction with AttachementFu. The largest file found in terms of bytes is returned.
Returns nil if there are not any video or image Files found.
# File lib/mms2r/media.rb, line 296 def default_media @default_media ||= attachment(['video', 'image', 'application', 'text']) end
Returns a File with the most likely candidate that is text, or nil otherwise. It also adds singleton methods to the File object so it can be used in place of a CGI upload (local_path, original_filename, size, and content_type) such as in conjunction with AttachmentFu. The largest file found in terms of bytes is returned.
Returns nil if there are not any text Files found
# File lib/mms2r/media.rb, line 308 def default_text @default_text ||= attachment(['text/plain']) end
Best guess of the mobile device type. Simple heuristics thus far by inspecting mail headers and jpeg/tiff exif metadata. Smart phone types are :iphone :blackberry :dash :droid If the message is from a carrier known to MMS2R, and not a smart phone its type is returned as :handset Otherwise device type is :unknown
# File lib/mms2r/media.rb, line 545 def device_type? begin # rely on native exif first with exifr gem if its loaded require 'exifr' file = attachment(['image']) if file original = file.original_filename @exif = case original when /\.je?pg$/i EXIFR::JPEG.new(file) when /\.tiff?$/i EXIFR::TIFF.new(file) end if @exif models = config['device_types']['models'] rescue {} models.each do |model, regex| return model if @exif.model =~ regex end end end rescue LoadError => err end headers = config['device_types']['headers'] rescue {} headers.keys.each do |header| if mail.header[header.downcase] # headers[header] refers to a hash of smart phone types with regex values # that if they match the header signals the type should be returned headers[header].each do |type, regex| # HACK to get at the full value of the header before TMail parses according to spec # see @body in and ensure_parsed in lib/tmail/header.rb return type if mail.header[header.downcase].instance_variable_get(:@body) =~ regex end end end return :handset if File.exist?(File.join(self.conf_dir, "#{self.aliases[self.carrier] || self.carrier}.yml")) :unknown end
exif object on default image from exifr gem
# File lib/mms2r/media.rb, line 591 def exif device_type? unless @exif @exif end
returns a filename declared for a part, or a default if its not defined
# File lib/mms2r/media.rb, line 508 def filename?(part) name = part.sub_header("content-type", "name") || part.sub_header("content-disposition", "filename") || (part['content-location'] && part['content-location'].to_s.strip) if (name.nil? || name.empty?) if part['content-id'] && part['content-id'].real_body.strip =~ /^<(.+)>$/ name = $1 else name = "#{Time.now.to_f}.#{self.default_ext(part.part_type?)}" end end # XXX fwiw, janky look for dot extension 1 to 4 chars long name = (name =~ /\..{1,4}$/ ? name : "#{name}.#{self.default_ext(part.part_type?)}") # handle excessively large filenames if name.size > 255 ext = File.extname(name) base = File.basename(name, ext) name = "#{base[0, 255 - ext.size]}#{ext}" end name end
Helper for process template method to determine if media contained in a part should be ignored. Producers should override this method to return true for media such as images that are advertising, carrier logos, etc. See the ignore section in the discussion of the built-in configuration.
# File lib/mms2r/media.rb, line 383 def ignore_media?(type,part) ignores = config['ignore'][type] || [] ignore = ignores.detect{|test| filename?(part) == test} ignore ||= ignores.detect{|test| filename?(part) =~ eval(test) if test.index('/') == 0 } ignore ||= ignores.detect{|test| part.body.strip =~ eval(test) if test.index('/') == 0 } ignore ||= (part.body.strip.size == 0 ? true : nil) ignore.nil? ? false : true end
convenience accessor for self.class.initialize_confg
# File lib/mms2r/media.rb, line 711 def initialize_config(config) self.class.initialize_config(config) end
The source of the MMS was some sort of mobile or smart phone
# File lib/mms2r/media.rb, line 599 def is_mobile? self.device_type? != :unknown end
# File lib/mms2r/media.rb, line 676 def log(message, level = :info) @logger.send(level, message) unless @logger.nil? end
Pass off everything we don’t do to the TMail object TODO: refactor to explicit addition a la blog.jayfields.com/2008/02/ruby-replace-methodmissing-with-dynamic.html
# File lib/mms2r/media.rb, line 119 def method_missing method, *args, &block mail.send method, *args, &block end
Get the phone number associated with this MMS if it exists. The value returned is simplistic, it is just the user name of the from address before the @ symbol. Validation of the number is left to you. Most carriers are using the real phone number as the username.
# File lib/mms2r/media.rb, line 248 def number unless @number params = config['number'] if params && (header = mail.header[params[0]]) @number = header.to_s.gsub(eval(params[1]), params[2]) end end @number ||= mail.from.first.split('@').first rescue "" end
process is a template method and collects all the media in a MMS. Override helper methods to this template to clean out advertising and/or ignore media that are advertising. This method should not be overridden unless there is an extreme special case in processing the media of a MMS (like Sprint)
Helper methods for the process template:
Block support: Call process() with a block to automatically iterate through media. For example, to process and receive only media of video type:
mms.process do |media_type, file|
results << file if media_type =~ /video/
end
note: purge must be explicitly called to remove the media files
mms2r extracts from an mms message.
# File lib/mms2r/media.rb, line 335 def process() # :yields: media_type, file unless @was_processed log("#{self.class} processing", :info) parts = mail.multipart? ? mail.parts : [mail] # Double check for multipart/related, if it exists replace it with its # children parts. Do this twice as multipart/alternative can have # children and we want to fold everything down for i in 1..2 flat = [] parts.each do |p| if MULTIPARTS_TO_SPLIT.include?(p.part_type?) p.parts.each {|mp| flat << mp } else flat << p end end parts = flat.dup end # get to work parts.each do |p| t = p.part_type? unless ignore_media?(t,p) t,f = process_media(p) add_file(t,f) unless t.nil? || f.nil? end end @was_processed = true end # when process acts upon a block if block_given? media.each do |k, v| yield(k, v) end end end
Helper for process template method to decode the part based on its type and write its content to a temporary file. Returns path to temporary file that holds the content. Parts with a main type of text will have their contents transformed with a call to transform_text
Producers should only override this method if the parts of the MMS need special treatment besides what is expected for a normal mime part (like Sprint).
Returns a tuple of content type, file path
# File lib/mms2r/media.rb, line 404 def process_media(part) # TMail body auto-magically decodes quoted # printable for text/html type. file = temp_file(part) if part.main_type('text') == 'text' || part.content_type == 'application/smil' type, content = transform_text_part(part) mode = 'w' else if part.content_type == 'application/octet-stream' type = type_from_filename(filename?(part)) else type = part.content_type end content = part.body mode = 'wb' # open with binary bit for Windows for non text end return type, nil if content.nil? || content.empty? log("#{self.class} writing file #{file}", :info) File.open(file, mode){ |f| f.write(content) } return type, file end
Purges the unique MMS2R::Media.media_dir directory created for this producer and all of the media that it contains.
# File lib/mms2r/media.rb, line 481 def purge() log("#{self.class} purging #{@media_dir} and all its contents", :info) FileUtils.rm_rf(@media_dir) end
convenience accessor for self.class.safe_message_id
# File lib/mms2r/media.rb, line 704 def safe_message_id(message_id) self.class.safe_message_id(message_id) end
Return the Subject for this message, returns “” for default carrier subject such as ‘Multimedia message’ for ATT&T carrier.
# File lib/mms2r/media.rb, line 262 def subject unless @subject subject = mail.subject.strip rescue "" ignores = config['ignore']['text/plain'] if ignores && ignores.detect{|s| s == subject} @subject = "" else @subject = transform_text('text/plain', subject).last end end @subject end
Helper for process template method to name a temporary filepath based on information in the part. This version attempts to honor the name of the media as labeled in the part header and creates a unique temporary directory for writing the file so filename collision does not occur. Consumers of this method expect the directory structure to the file exists, if the method is overridden it is mandatory that this behavior is retained.
# File lib/mms2r/media.rb, line 472 def temp_file(part) file_name = filename?(part) File.join(msg_tmp_dir(),File.basename(file_name)) end
convenience accessor for self.class.conf_dir
# File lib/mms2r/media.rb, line 690 def tmp_dir self.class.tmp_dir end
Helper for process_media template method to transform text. See the transform section in the discussion of the built-in configuration.
# File lib/mms2r/media.rb, line 433 def transform_text(type, text, original_nencoding = 'ISO-8859-1') return type, text unless transforms = config['transform'][type] #convert to UTF-8 begin c = Iconv.new('UTF-8', original_nencoding ) utf_t = c.iconv(text) rescue Exception => e utf_t = text end transforms.each do |transform| next unless transform.size == 2 p = transform.first r = transform.last utf_t = utf_t.gsub(eval(p), r) rescue utf_t end return type, utf_t end
Helper for process_media template method to transform text.
# File lib/mms2r/media.rb, line 457 def transform_text_part(part) type = part.part_type? text = part.body.strip transform_text(type, text) end