vpim-0.695/0000755000076500000240000000000011152674120011152 5ustar samstaffvpim-0.695/CHANGES0000644000076500000240000003547511152674120012163 0ustar samstaff0.695 - 2009-03-01 - Alpha versions of vPim agent (http://agent.octetcloud.com) - Fixed: reminder app had drifted out of sync with API change 0.657 - 2008-08-14 - Date#to_time renamed to Date#vpim_to_time. Apparently Rails also patches the core. - Debian doesn't like RFCs. I think there is a problem with the DFG, not the IETF. The value of a standard is that it can't be modified (if anybody could publish a modified version of an RFC without restriction chaos would ensue). Still, we lost that argument, and I'll try and help packagers out. - View, gives various views of calendars. Experimental. - Fixed doc comment refs to common Property module. - Vtodo#duration and #due now work correctly (the missing one will be calculated from the other). - Vevent::Maker#add_rrule, #set_rrule are new, using Rrule::Maker. - There is now (half-of) an Rrule::Maker class to help construct RRULE values. - Vjournal can be encoded now (it was broken). - Better code coverage by the tests. - Recurrence#occurrences now returns an Enumerator instead of an Rrule. Both have #each, and both yield the same thing, so that isn't an API change. However, Rrule had an each_until, and the Enumerator doesn't. Use the dountil argument to #occurrences to get the same effect. The asymetry of this API caused me (non-aesthetic) trouble, now Rrule is an internal implementation detail. Also, this would have had to change eventually, because occurrences need to be the union of multiple RRULE and RDATE fields. - Recurrence#rrule, returns an Rrule for the first RRULE field. Can be used as transition. - Icalendar#calscale was broken, and is now unit tested, along with #version and #protocol?. - Icalendar#{each,events,todos,journals} all yield components, or return an enumerator. - Icalendar is enumerable. - Repo and Repo::Calendar are enumerable. - #occurrences will call Rrule#each if a block is provided - Recurrence rules with DTSTART in UTC will now sortof work (thanks to Max Werner for providing the patch). - Added convenience methods for setting and getting title and org fiels (thanks to Jade Meskill for providing the patch). - Modified Icalendar#create2 so only prodid can be supplied and cal is yielded so events/etc. can be pushed. - Support Highrisehq.com's broken google talk field (see test_vcard.rb for examples, thanks to Terry Tong for reporting). 0.619 - 2008-03-30 - Fixed some problems with rescue statements not being specific enough. - Vcard#birthday may now return a DateTime. - Added Vcard#urls - Fixed a mispelled Uri in Vcard#url - Global fix of misspellings of "occurrence" - KAddressBook compatibility fix - allow / in field names, even though it is illegal in vCard 3.0. 0.604 - 2008-03-13 - Fixed a bug with lower-case UTF-16 encoded cards not being detected properly. - Skip over invalidly encoded vCard fields when enumerating them. 0.602 - 2008-03-12 - Updated reminder utility to work with iCal 3. - Reworked gem to include tests, samples, and binaries. 0.597 - 2008-03-01 - Support for BYSETPOS in recurrence rules (development supported by ZipDX.com) - Support for FREQ=weekly in recurrence rules (development supported by ZipDX.com) - Fixed an encode_text() bug (patch supplied by Jan Becvar) - Fixed problem with interaction between BYMONTH and BYDAY (patch supplied by Sam Stephenson of 32signals.com) - Vevent::Maker - Started adding high-level iCalendar encoding APIs - Vpim::Vcard::Maker - the vCard maker is moved here - Vpim::Maker - deprecated - Vcard::make, Maker::Vcard#delete_if - new - Vcard::Maker#add_name - deprecate, see Vcard::Maker#name - Vcard::Maker#fullname= - deprecate, see Vcard::Maker#name - Added high-level vCard decoding API. - Beginning to depend on ruby1.8. I'm willing to try for backwards compat if I get feedback that this causes difficulties. - Icalendar#components - new - Icalendar#vevents - deprecated, see Icalendar#components - split Vtodo and Vjournal into own files - Added Recurrence module to Vtodo and Vjournal - Property::Common#sequence - new - Property::Common#attachments - new - Icalendar::Attachment - new - Maker::Vcard#add_url - new - ex_mkyourown.rb - new example - Modularized the component property accessor methods, and added lots of Icalendar property support for Vevent, Vtodo, and Vjournal. - Fixed support for TEXT decoding. - Use Subversion revision as release sub-version. - It appears that a top-level vpim.rb that requires everything else is needed for ruby-gems, gemspec seems to work now. - Don't package backup files (.../*.rb~). - Icalendar decoding optimizations. Icalendar.decode is about twice as fast now, and more optimizations are scheduled. - Continue the move to using uppercase for all syntactic elements that can be mixed case. The API might eventually allow only uppercase, it appears case-insensitive comparisons still have a noticeable effect on performance. 0.17 - 2006-03-08 - DirectoryInfo#delete - new - Maker::Vcard#fullname - new - Provide an example of how to create, copy, and modify version 2.1 vCards. - Maker::Vcard.make2 - new - Maker::Vcard.make - deprecated - Profiled decoding of a huge iCalendar file. Performance appears to be dominated by overhead of String#downcase (20% of time spent in Field#name?). Keeping the field group, field name, and field's parameter's names internally in uppercase and using ruby 1.8's String#casecmp? is a first attempt at optimization. This is a change in the default case returned, but it aligns with the RFC and common usage. - Maker::Vcard.make - full_name is now optional, it will be derived from name/N: - Maker::Vcard#add_field - better argument checking - Maker::Vcard#copy - new - Vcard#[] - now limits return to fields with values - Vcard#name - new - Vcard::Name - new - Vcard#nicknames - new - Field#params and Field#param - deprecated and undocumented, I could never remember the difference - Field#pnames - new - Field#pvalues - new - Methods.casecmp? - new - Field#pref= - new - Field#pvalue_idel - new - Field#pvalue_iadd - new 0.16 - 2006-02-19 - Packaged in gem format, experimental. - Read vCards in UTF-8 or UTF-16, big or little endian, with or without a byte order mark (BOM). - Fixed bug with rrule occurences containing the end of the interval. Reported with patch by Brad Ediger. 0.15 - 2005-07-29 - Fixed bug with param values with quoted strings, particularly a quoted string containing a : character, like name;param="file:///":value Thanks to Tamiji Homma for reporting this and sending me an example calendar. - Vtodo.create - new - Icalendar#push - allow Vtodos to be added Thanks to Maximillian Dornseif for contributing the above two Vtodo patches. - Vevent.duration - bug fixes so duration can be calculated from begin/end, and end can be calculated from begin/duration - Vcard#nickname that strips whitespace to see if there really is a nickname - Vcard#birthday - returns birthday as a date - Vevent#create_yearly - easy, cheesy way to create yearly recurring events - vcf-to-ics.rb: example of how to create calendars of bdays from vcards - Icalendar.create() like Vevent.create(), it will take arrays of Fields, or Hashes of String => value. - maker/vcard.rb: Added support for X-AIM, an Apple extension. 0.14 - 2005-02-01 - Fix: if an RRULE didn't ever yield an event (a bug in the rule) dountil was never tested. - Change: don't throw an ArgumentError with infinite events, just stop when the Time is no longer representable 0.13 - 2005-01-20 - Was calling to_time with an arg, fixed. 0.12 - 2005-01-17 - Removed require of pp from the library and utilities where it wasn't needed, it was causing problems because it doesn't exist on ruby 1.6 systems. - Added Field#to_date, returns field value as an array of Date objects - Changed Field#to_time - it now auto-detects format of values (DATE/DATE-TIME), and doesn't take/require a default_kind argument. - Added IMPP support to Vpim::Maker::Vcard. - Makefile wasn't copying the Maker classes into the release. - Duration value not returned unless it was negative, fixed. - An RRULE's UNTIL was always being assumed to be a DATE-TIME, but it can be a DATE. I fixed this, but is something of a hack, see comments and TODO. - Use String#scan instead of String#gsub, when appropriate (I didn't know it existed when I stated the project). - Run tests on 3 ruby versions. - Don't include docs in release package, it makes it way too large. - Change: use String.unpack instead of iconv to convert UCS-2 to UTF-8, removing dependency on iconv (it wasn't standard in ruby 1.6). - Change: simplified mutt_ab_query.rb, and renamed to vcf-to-mutt. - Change: reminder.rb sorts todo items by priority. - New: Vtodo#priority - the priority of the vTodo component. 0.11 - 2004-11-17 - Added a Vpim::Maker::Vcard class to simplify the creation of vCards, modelled after the RSS maker from ruby's RSS library. 0.10 - 2004-11-07 - If events don't have a recurrence rule, they occur once, but they were being returned even if they occurred after "dountil". Fixed. - New sample of converting tab-delimited files to a vcard file, from Dane G. Avilla. Thanks! 0.9a - 2004-10-31 - Made sure all events occur once in rrules. - New sample of encoding: mutt-aliases-vcard.rb - Added ToDo support to reminder.rb. 0.9 - 2004-06-17 - Field now is mutable, you can change the group, value, params, etc. - Using the Enumerator object for DirectoryInfo now, instead of all the each_by_X, and field_by_X() APIs. - Moved homepage and docs to Ruby Forge. - DirectoryInfo.create: added a profile argument - DirectoryInfo#push: now pushes to 1 before the end - DirectoryInfo#push_end: pushes onto end, in case you really want to - Field.create: a Date or Time object value will now be encoded as date or time - Vpim.encode_date(): encodes an RFC2425 date - Vpim.encode_time(): encodes an RFC2425 time - Vpim.encode_date_time(): encodes an RFC2425 date-time - Icalendar#encode(): encodes an Icalendar - Icalendar#push(): pushes a calendar component onto a calendar - Vevent#accept(): accepts an event invitation - Vevent#create(): creates a new event - Address#copy(): create a copy of Address. If the original Address was frozen, this one won't be. - Address#partstat=(): set or change the PARTSTAT. - Field#copy(): create a copy of Field. If the original Field was frozen, this one won't be. 0.8 - 2004-04-01 - Moved DirectoryInfo::Field into it's own file, vpim/field.rb. - New: Vpim::Duration - crude way of getting days/hours/mins/secs from a duration in seconds. - New: Icalendar#create() and Icalendar#create_reply() - New: Icalendar#encode(), #to_s is an alias to #encode. - New: Icalendar#protocol?() - Change: Icalendar#version() raises an error if there is no VERSION - Change: made all the DirectoryInfo, Vcard, and Field .new() class methods private, and replaced with the 2 class methods: - decode() decodes a string, returning a ruby object - create() creates a new object and all objects now get encoded using: - encode() takes a ruby object, and encodes it as a string They become more symetrical, and the overloaded meanings of .new() dissappear - with .new() are you decoding, encoding, creating...? - Change: Icalendar::Vevent#attendees() can return only attendees with a particular URI. - New: Icalendar::Address#==() - New: Icalendar::Vevent#attendee?() - Fixed bug: Field#encode() was adding an unexpecte NL to the line. - Change: Field#name?() can accept a symbol. - New: DirectoryInfo has an #each(), so I included Enumerable. Because of this #to_a() now returns all the Fields in a DirectoryInfo. - New: DirectoryInfo#push() and DirectoryInfo@push_uniq(). - Change: DirectoryInfo#<<() is now an alias for DirectoryInfo#push() - Change: DirectoryInfo#each() and DirectoryInfo#each_by() now return self instead of nil, I think this is more rubyish. - New: Date#to_time() - converts a Date, and maybe a DateTime, to a Time - New: Icalendar#protocol() - New: Icalendar::Address - New: Icalendar::Vevent#organizer()/attendees(), which return an Icalendar::Address - New: An array of all the values of fields named name, converted to text, using Field.to_text(). 0.7 - 2004-03-21 - Bug fixes, not all files were requiring vpim.rb, which had the definition of the invalid encoding error. - Implemented much requested feature: ignore empty lines in input. This is an invalid encoding, but I'm tired of fighting it. 0.6 - 2004-03-20 WARNING: major API renamings! - Replaced the Vpim::Errors::*Error exception classes with a single exception class: Vpim::InvalidEncodingError. That, and a message, is all I really need. I don't think people need different classes for different types of encoding errors, either the library can decode it for them, or it can't. - Renamed project to "vpim" (to reflect vCard and vCalendar/iCalendar support), renamed top-level Rfc2425 module to Vpim, split vard.rb into multiple files. - Add support for iCalendar (RFC2445), see vpim/icalendar.rb. Currently only supports VEVENT, no VTODO or VALARM yet, but is useable to print my upcoming iCal events. - Implemented the iCalendar recurrence rules mini-language, which is possibly of more general use than just for iCalendar. - Fixed bug in time decoding where usec would be nil instead of zero when no usec were present in the time value. - Field#to_time now assumes that time is in local time, unless the timezone is "Z", meaning UTC. - Field#to_text - new method, unescapes newlines, commas, and escape characters. - Field#field(name) - new method, returns the first field named +name+. 0.5 - 3003-11-23 - ab-query.rb - short option for --me was mistyped as -v, instead of -m. - mutt_ab_query.rb - added a --pipe option, so that the output of an other script can be directly queried. - New method: Rfc2425.version - Decode vCard 2.1 abbreviated parameters (ones where the name of the parameter is missing, only the value is present, which only works for type and encoding). - Vcard.decode() now support UCS-2 encoded vCards, by translating anything that looks like UCS-2 to UTF-8 before decoding. 0.4 - 2003-04-14 - More support for decoding date, time, and date-time values. - New method Field#to_time(). - Can pass an IO object to decode APIs, its entire contents is read as a string. - Field#group?() now considers nil as equivalent to no group, so you can use each_group(nil) to iterate through all fields without a group. 0.3 - - Added description of how to use mutt_ab_query.rb - Added support for querying the kind of a value, and began support for decoding date and time values. 0.2 - - Supports encoding. - Supports accessing values using []. - No longer have methods return an Array, or nil if the array is zero length, I just return an Array. - mutt_ab_query.rb - an example of using vcard.rb to do lookups in the OS X Address Book from Mutt 0.1 - - First release. --- vim:expandtab:tabstop=8:softtabstop=2:shiftwidth=2 vpim-0.695/COPYING0000644000076500000240000000470711152674120012215 0ustar samstaffvPim is copyrighted free software by Sam Roberts . You can redistribute it and/or modify it under either the terms of the GPL (see the file GPL), or the conditions below: 1. You may make and give away verbatim copies of the source form of the software without restriction, provided that you duplicate all of the original copyright notices and associated disclaimers. 2. You may modify your copy of the software in any way, provided that you do at least ONE of the following: a) place your modifications in the Public Domain or otherwise make them Freely Available, such as by posting said modifications to Usenet or an equivalent medium, or by allowing the author to include your modifications in the software. b) use the modified software only within your corporation or organization. c) give non-standard binaries non-standard names, with instructions on where to get the original software distribution. d) make other distribution arrangements with the author. 3. You may distribute the software in object code or binary form, provided that you do at least ONE of the following: a) distribute the binaries and library files of the software, together with instructions (in the manual page or equivalent) on where to get the original distribution. b) accompany the distribution with the machine-readable source of the software. c) give non-standard binaries non-standard names, with instructions on where to get the original software distribution. d) make other distribution arrangements with the author. 4. You may modify and include the part of the software into any other software (possibly commercial). But some files in the distribution are not written by the author, so that they are not under these terms. For the list of those files and their copying conditions, see the file LEGAL. 5. The scripts and library files supplied as input to or produced as output from the software do not automatically fall under the copyright of the software, but belong to whomever generated them, and may be sold commercially, and may be aggregated with this software. 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. vpim-0.695/etc/0000755000076500000240000000000011152674120011725 5ustar samstaffvpim-0.695/lib/0000755000076500000240000000000011152674120011720 5ustar samstaffvpim-0.695/lib/atom.rb0000644000076500000240000005200511152674120013207 0ustar samstaff# Copyright (c) 2008 The Kaphan Foundation # # For licensing information see LICENSE.txt. # # Please visit http://www.peerworks.org/contact for further information. # require 'forwardable' require 'delegate' require 'rubygems' gem 'libxml-ruby' require 'xml/libxml' require 'atom/xml/parser.rb' module Atom # :nodoc: NAMESPACE = 'http://www.w3.org/2005/Atom' unless defined?(NAMESPACE) module Pub NAMESPACE = 'http://www.w3.org/2007/app' end # Raised when a Parsing Error occurs. class ParseError < StandardError; end # Raised when a Serialization Error occurs. class SerializationError < StandardError; end # Provides support for reading and writing simple extensions as defined by the Atom Syndication Format. # # A Simple extension is an element from a non-atom namespace that has no attributes and only contains # text content. It is interpreted as a key-value pair when the namespace and the localname of the # extension make up the key. Since in XML you can have many instances of an element, the values are # represented as an array of strings, so to manipulate the values manipulate the array returned by # +[ns, localname]+. # module SimpleExtensions attr_reader :simple_extensions # Gets a simple extension value for a given namespace and local name. # # +ns+:: The namespace. # +localname+:: The local name of the extension element. # def [](ns, localname) if !defined?(@simple_extensions) || @simple_extensions.nil? @simple_extensions = {} end key = "{#{ns},#{localname}}" (@simple_extensions[key] or @simple_extensions[key] = ValueProxy.new) end class ValueProxy < DelegateClass(Array) attr_accessor :as_attribute def initialize super([]) @as_attribute = false end end end # Represents a Generator as defined by the Atom Syndication Format specification. # # The generator identifies an agent or engine used to a produce a feed. # # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.generator class Generator include Xml::Parseable attr_accessor :name attribute :uri, :version # Initialize a new Generator. # # +xml+:: An XML::Reader object. # def initialize(o = nil) # :yield: self case o when Hash o.each do |k, v| self.send("#{k.to_s}=", v) end when nil else @name = o.read_string.strip parse(o, :once => true) end yield(self) if block_given? end end # Represents a Category as defined by the Atom Syndication Format specification. # # class Category include Atom::Xml::Parseable include SimpleExtensions attribute :label, :scheme, :term def initialize(o = nil) # :yield: self case o when XML::Reader parse(o, :once => true) when Hash o.each do |k, v| self.send("#{k.to_s}=", v) end when nil else raise ArgumentError, "Don't know how to handle #{o}" end yield(self) if block_given? end end # Represents a Person as defined by the Atom Syndication Format specification. # # A Person is used for all author and contributor attributes. # # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#atomPersonConstruct # class Person include Xml::Parseable element :name, :uri, :email # Initialize a new person. # # +o+:: An XML::Reader object or a hash. Valid hash keys are +:name+, +:uri+ and +:email+. def initialize(o = {}) case o when Hash o.each do |k, v| self.send("#{k.to_s}=", v) end else o.read parse(o) end end def inspect " true) end end def to_xml(nodeonly = true, name = 'content', namespace = nil, namespace_map = Atom::Xml::NamespaceMap.new) node = XML::Node.new("#{namespace_map.prefix(Atom::NAMESPACE, name)}") node << self.to_s node end end # Html content within an Atom document. class Html < Base attribute :type, :'xml:lang' # Creates a new Content::Html. # # +o+:: An XML::Reader or a HTML string. # def initialize(o) case o when String super(o) @type = 'html' else super(o.read_string) parse(o, :once => true) end end def to_xml(nodeonly = true, name = 'content', namespace = nil, namespace_map = Atom::Xml::NamespaceMap.new) # :nodoc: require 'iconv' # Convert from utf-8 to utf-8 as a way of making sure the content is UTF-8. # # This is a pretty crappy way to do it but if we don't check libxml just # fails silently and outputs the content element without any content. At # least checking here and raising an exception gives the caller a chance # to try and recitfy the situation. # begin node = XML::Node.new("#{namespace_map.prefix(Atom::NAMESPACE, name)}") node << Iconv.iconv('utf-8', 'utf-8', self.to_s) node['type'] = 'html' node['xml:lang'] = self.xml_lang if self.xml_lang node rescue Iconv::IllegalSequence => e raise SerializationError, "Content must be converted to UTF-8 before attempting to serialize to XML: #{e.message}." end end end # XHTML content within an Atom document. class Xhtml < Base XHTML = 'http://www.w3.org/1999/xhtml' attribute :type, :'xml:lang' def initialize(xml) super("") parse(xml, :once => true) starting_depth = xml.depth # Get the next element - should be a div according to the atom spec while xml.read == 1 && xml.node_type != XML::Reader::TYPE_ELEMENT; end if xml.local_name == 'div' && xml.namespace_uri == XHTML set_content(xml.read_inner_xml.strip.gsub(/\s+/, ' ')) else set_content(xml.read_outer_xml) end # get back to the end of the element we were created with while xml.read == 1 && xml.depth > starting_depth; end end def to_xml(nodeonly = true, name = 'content', namespace = nil, namespace_map = Atom::Xml::NamespaceMap.new) node = XML::Node.new("#{namespace_map.prefix(Atom::NAMESPACE, name)}") node['type'] = 'xhtml' node['xml:lang'] = self.xml_lang div = XML::Node.new('div') div['xmlns'] = XHTML p = XML::Parser.string(to_s) content = p.parse.root.copy(true) div << content node << div node end end end # Represents a Source as defined by the Atom Syndication Format specification. # # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.source class Source extend Forwardable def_delegators :@links, :alternate, :self, :alternates, :enclosures include Xml::Parseable element :id element :updated, :class => Time, :content_only => true element :title, :subtitle, :class => Content elements :authors, :contributors, :class => Person elements :links def initialize(o = nil) @authors, @contributors, @links = [], [], Links.new case o when XML::Reader unless current_node_is?(o, 'source', NAMESPACE) raise ArgumentError, "Invalid node for atom:source - #{o.name}(#{o.namespace})" end o.read parse(o) when Hash o.each do |k, v| self.send("#{k.to_s}=", v) end end yield(self) if block_given? end end # Represents a Feed as defined by the Atom Syndication Format specification. # # A feed is the top level element in an atom document. It is a container for feed level # metadata and for each entry in the feed. # # This supports pagination as defined in RFC 5005, see http://www.ietf.org/rfc/rfc5005.txt # # == Parsing # # A feed can be parsed using the Feed.load_feed method. This method accepts a String containing # a valid atom document, an IO object, or an URI to a valid atom document. For example: # # # Using a File # feed = Feed.load_feed(File.open("/path/to/myfeed.atom")) # # # Using a URL # feed = Feed.load_feed(URI.parse("http://example.org/afeed.atom")) # # == Encoding # # A feed can be converted to XML using, the to_xml method that returns a valid atom document in a String. # # == Attributes # # A feed has the following attributes: # # +id+:: A unique id for the feed. # +updated+:: The Time the feed was updated. # +title+:: The title of the feed. # +subtitle+:: The subtitle of the feed. # +authors+:: An array of Atom::Person objects that are authors of this feed. # +contributors+:: An array of Atom::Person objects that are contributors to this feed. # +generator+:: A Atom::Generator. # +categories+:: A list of Atom:Category objects for the feed. # +rights+:: A string describing the rights associated with this feed. # +entries+:: An array of Atom::Entry objects. # +links+:: An array of Atom:Link objects. (This is actually an Atom::Links array which is an Array with some sugar). # # == References # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.feed # class Feed include Xml::Parseable include SimpleExtensions extend Forwardable def_delegators :@links, :alternate, :self, :via, :first_page, :last_page, :next_page, :prev_page loadable! namespace Atom::NAMESPACE element :id, :rights element :generator, :class => Generator element :title, :subtitle, :class => Content element :updated, :class => Time, :content_only => true elements :links elements :authors, :contributors, :class => Person elements :categories elements :entries # Initialize a Feed. # # This will also yield itself, so a feed can be constructed like this: # # feed = Feed.new do |feed| # feed.title = "My Cool feed" # end # # +o+:: An XML Reader or a Hash of attributes. # def initialize(o = {}) @links, @entries, @authors, @contributors, @categories = Links.new, [], [], [], [] case o when Hash o.each do |k, v| self.send("#{k.to_s}=", v) end else if next_node_is?(o, 'feed', Atom::NAMESPACE) o.read parse(o) else raise ArgumentError, "XML document was missing atom:feed: #{o.read_outer_xml}" end end yield(self) if block_given? end # Return true if this is the first feed in a paginated set. def first? links.self == links.first_page end # Returns true if this is the last feed in a paginated set. def last? links.self == links.last_page end # Reloads the feed by fetching the self uri. def reload!(opts = {}) if links.self Feed.load_feed(URI.parse(links.self.href), opts) end end # Iterates over each entry in the feed. # # ==== Options # # +paginate+:: If true and the feed supports pagination this will fetch each page of the feed. # +since+:: If a Time object is provided each_entry will iterate over all entries that were updated since that time. # +user+:: User name for HTTP Basic Authentication. # +pass+:: Password for HTTP Basic Authentication. # def each_entry(options = {}, &block) if options[:paginate] since_reached = false feed = self loop do feed.entries.each do |entry| if options[:since] && entry.updated && options[:since] > entry.updated since_reached = true break else block.call(entry) end end if since_reached || feed.next_page.nil? break else feed.next_page feed = feed.next_page.fetch(options) end end else self.entries.each(&block) end end end # Represents an Entry as defined by the Atom Syndication Format specification. # # An Entry represents an individual entry within a Feed. # # == Parsing # # An Entry can be parsed using the Entry.load_entry method. This method accepts a String containing # a valid atom entry document, an IO object, or an URI to a valid atom entry document. For example: # # # Using a File # entry = Entry.load_entry(File.open("/path/to/myfeedentry.atom")) # # # Using a URL # Entry = Entry.load_entry(URI.parse("http://example.org/afeedentry.atom")) # # The document must contain a stand alone entry element as described in the Atom Syndication Format. # # == Encoding # # A Entry can be converted to XML using, the to_xml method that returns a valid atom entry document in a String. # # == Attributes # # An entry has the following attributes: # # +id+:: A unique id for the entry. # +updated+:: The Time the entry was updated. # +published+:: The Time the entry was published. # +title+:: The title of the entry. # +summary+:: A short textual summary of the item. # +authors+:: An array of Atom::Person objects that are authors of this entry. # +contributors+:: An array of Atom::Person objects that are contributors to this entry. # +rights+:: A string describing the rights associated with this entry. # +links+:: An array of Atom:Link objects. (This is actually an Atom::Links array which is an Array with some sugar). # +source+:: Metadata of a feed that was the source of this item, for feed aggregators, etc. # +categories+:: Array of Atom::Categories. # +content+:: The content of the entry. This will be one of Atom::Content::Text, Atom::Content:Html or Atom::Content::Xhtml. # # == References # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.entry for more detailed # definitions of the attributes. # class Entry include Xml::Parseable include SimpleExtensions extend Forwardable def_delegators :@links, :alternate, :self, :alternates, :enclosures, :edit_link, :via loadable! namespace Atom::NAMESPACE element :title, :id, :summary element :updated, :published, :class => Time, :content_only => true element :source, :class => Source elements :links elements :authors, :contributors, :class => Person elements :categories, :class => Category element :content, :class => Content # Initialize an Entry. # # This will also yield itself, so an Entry can be constructed like this: # # entry = Entry.new do |entry| # entry.title = "My Cool entry" # end # # +o+:: An XML Reader or a Hash of attributes. # def initialize(o = {}) @links = Links.new @authors = [] @contributors = [] @categories = [] case o when Hash o.each do |k,v| send("#{k.to_s}=", v) end else if current_node_is?(o, 'entry', Atom::NAMESPACE) || next_node_is?(o, 'entry', Atom::NAMESPACE) o.read parse(o) else raise ArgumentError, "Entry created with node other than atom:entry: #{o.name}" end end yield(self) if block_given? end # Reload the Entry by fetching the self link. def reload!(opts = {}) if links.self Entry.load_entry(URI.parse(links.self.href), opts) end end end # Links provides an Array of Link objects belonging to either a Feed or an Entry. # # Some additional methods to get specific types of links are provided. # # == References # # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.link # for details on link selection and link attributes. # class Links < DelegateClass(Array) include Enumerable # Initialize an empty Links array. def initialize super([]) end # Get the alternate. # # Returns the first link with rel == 'alternate' that matches the given type. def alternate(type = nil) detect { |link| (link.rel.nil? || link.rel == Link::Rel::ALTERNATE) && (type.nil? || type == link.type) } end # Get all alternates. def alternates select { |link| link.rel.nil? || link.rel == Link::Rel::ALTERNATE } end # Gets the self link. def self detect { |link| link.rel == Link::Rel::SELF } end # Gets the via link. def via detect { |link| link.rel == Link::Rel::VIA } end # Gets all links with rel == 'enclosure' def enclosures select { |link| link.rel == Link::Rel::ENCLOSURE } end # Gets the link with rel == 'first'. # # This is defined as the first page in a pagination set. def first_page detect { |link| link.rel == Link::Rel::FIRST } end # Gets the link with rel == 'last'. # # This is defined as the last page in a pagination set. def last_page detect { |link| link.rel == Link::Rel::LAST } end # Gets the link with rel == 'next'. # # This is defined as the next page in a pagination set. def next_page detect { |link| link.rel == Link::Rel::NEXT } end # Gets the link with rel == 'prev'. # # This is defined as the previous page in a pagination set. def prev_page detect { |link| link.rel == Link::Rel::PREVIOUS } end # Gets the edit link. # # This is the link which can be used for posting updates to an item using the Atom Publishing Protocol. # def edit_link detect { |link| link.rel == 'edit' } end end # Represents a link in an Atom document. # # A link defines a reference from an Atom document to a web resource. # # == References # See http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.link for # a description of the different types of links. # class Link module Rel # :nodoc: ALTERNATE = 'alternate' SELF = 'self' VIA = 'via' ENCLOSURE = 'enclosure' FIRST = 'first' LAST = 'last' PREVIOUS = 'prev' NEXT = 'next' end include Xml::Parseable attribute :href, :rel, :type, :length # Create a link. # # +o+:: An XML::Reader containing a link element or a Hash of attributes. # def initialize(o = nil) # :yield: self case o when Hash [:href, :rel, :type, :length].each do |attr| self.send("#{attr}=", o[attr]) end when nil else if current_node_is?(o, 'link') parse(o, :once => true) else raise ArgumentError, "Link created with node other than atom:link: #{o.name}" end end yield(self) if block_given? end remove_method :length= def length=(v) @length = v.to_i end def to_s self.href end def ==(o) o.respond_to?(:href) && o.href == self.href end # This will fetch the URL referenced by the link. # # If the URL contains a valid feed, a Feed will be returned, otherwise, # the body of the response will be returned. # # TODO: Handle redirects. # def fetch(options = {}) begin Atom::Feed.load_feed(URI.parse(self.href), options) rescue ArgumentError, ParseError => ae Net::HTTP.get_response(URI.parse(self.href)).body end end def inspect "" end end end vpim-0.695/lib/plist.rb0000644000076500000240000000116611152674120013404 0ustar samstaff#-- ############################################################## # Copyright 2006, Ben Bleything and # # Patrick May # # # # Distributed under the MIT license. # ############################################################## #++ # = Plist # # This is the main file for plist. Everything interesting happens in Plist and Plist::Emit. require 'base64' require 'cgi' require 'stringio' require 'plist/generator' require 'plist/parser' module Plist VERSION = '3.0.0' end vpim-0.695/lib/vpim/0000755000076500000240000000000011152674120012673 5ustar samstaffvpim-0.695/lib/vpim/address.rb0000644000076500000240000001314411152674120014650 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end =begin Notes on a CAL-ADDRESS When used with ATTENDEE, the parameters are: CN CUTYPE DELEGATED-FROM DELEGATED-TO DIR LANGUAGE MEMBER PARTSTAT ROLE RSVP SENT-BY When used with ORGANIZER, the parameters are: CN DIR LANGUAGE SENT-BY What I've seen in Notes invitations, and iCal responses: ROLE PARTSTAT RSVP CN Support these last 4, for now. =end module Vpim class Icalendar # Used to represent calendar fields containing CAL-ADDRESS values. # The organizer or the attendees of a calendar event are examples of such # a field. # # Example: # # ORGANIZER;CN="A. Person":mailto:a_person@example.com # # ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION # ;CN="Sam Roberts";RSVP=TRUE:mailto:SRoberts@example.com # class Address # Create a copy of Address. If the original Address was frozen, this one # won't be. def copy #Marshal.load(Marshal.dump(self)) self.dup.dirty end def dirty #:nodoc: @field = nil self end # Addresses in a CAL-ADDRESS are represented as a URI, usually a mailto URI. attr_accessor :uri # The common or displayable name associated with the calendar address, or # nil if there is none. attr_accessor :cn # The participation role for the calendar user specified by the address. # # The standard roles are: # - CHAIR Indicates chair of the calendar entity # - REQ-PARTICIPANT Indicates a participant whose participation is required # - OPT-PARTICIPANT Indicates a participant whose participation is optional # - NON-PARTICIPANT Indicates a participant who is copied for information purposes only # # The default role is REQ-PARTICIPANT, returned if no ROLE parameter was # specified. attr_accessor :role # The participation status for the calendar user specified by the # property PARTSTAT, a String. # # These are the participation statuses for an Event: # - NEEDS-ACTION Event needs action # - ACCEPTED Event accepted # - DECLINED Event declined # - TENTATIVE Event tentatively accepted # - DELEGATED Event delegated # # Default is NEEDS-ACTION. # # FIXME - make the default depend on the component type. attr_accessor :partstat # The value of the RSVP field, either +true+ or +false+. It is used to # specify whether there is an expectation of a reply from the calendar # user specified by the property value. attr_accessor :rsvp def initialize(field=nil) #:nodoc: @field = field @uri = '' @cn = '' @role = "REQ-PARTICIPANT" @partstat = "NEEDS-ACTION" @rsvp = false end # Create a new Address. It will encode as a +name+ property. def self.create(uri='') adr = new adr.uri = uri.to_str adr end def self.decode(field) adr = new(field) adr.uri = field.value cn = field.param('CN') if cn adr.cn = cn.first end role = field.param('ROLE') if role adr.role = role.first.strip.upcase end partstat = field.param('PARTSTAT') if partstat adr.partstat = partstat.first.strip.upcase end rsvp = field.param('RSVP') if rsvp adr.rsvp = case rsvp.first when /TRUE/i then true when /FALSE/i then false else raise InvalidEncodingError, "RSVP param value not TRUE/FALSE: #{rsvp}" end end adr.freeze end # Return a representation of this Address as a DirectoryInfo::Field. def encode(name) #:nodoc: if @field # FIXME - set the field name, it could be different from cached return @field end value = uri.to_str.strip if value.empty? raise Uencodeable, "Address#uri is zero-length" end params = {} if cn.length > 0 params['CN'] = Vpim::encode_paramvalue(cn) end # FIXME - default value is different for non-vEvent if role.length > 0 && role != 'REQ-PARTICIPANT' params['ROLE'] = Vpim::encode_paramtext(role) end # FIXME - default value is different for non-vEvent if partstat.length > 0 && partstat != 'NEEDS-ACTION' params['PARTSTAT'] = Vpim::encode_paramtext(partstat) end if rsvp params['RSVP'] = 'true' end Vpim::DirectoryInfo::Field.create(name, value, params) end # Return true if the +uri+ is == to this address' URI. The comparison # is case-insensitive (because email addresses and domain names are). def ==(uri) # TODO - could I use a URI library? Vpim::Methods.casecmp?(self.uri.to_str, uri.to_str) end # A string representation of an address, using the common name, and the # URI. The URI protocol is stripped if it's "mailto:". def to_s u = uri u = u.gsub(/^mailto: */i, '') if cn.length > 0 "#{cn.inspect} <#{uri}>" else uri end end def inspect #:nodoc: "#" end end end end vpim-0.695/lib/vpim/attachment.rb0000644000076500000240000000567411152674120015364 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require 'vpim/icalendar' module Vpim # Attachments are used by both iCalendar and vCard. They are either a URI or # inline data, and their decoded value will be either a Uri or a Inline, as # appropriate. # # Besides the methods specific to their class, both kinds of object implement # a set of common methods, allowing them to be treated uniformly: # - Uri#to_io, Inline#to_io: return an IO from which the value can be read. # - Uri#to_s, Inline#to_s: return the value as a String. # - Uri#format, Inline#format: the format of the value. This is supposed to # be an "iana defined" identifier (like "image/jpeg"), but could be almost # anything (or nothing) in practice. Since the parameter is optional, it may # be "". # # The objects can also be distinguished by their class, if necessary. module Attachment # TODO - It might be possible to autodetect the format from the first few # bytes of the value, and return the appropriate MIME type when format # isn't defined. # # iCalendar and vCard put the format in different parameters, and the # default kind of value is different. def Attachment.decode(field, defkind, fmtparam) #:nodoc: format = field.pvalue(fmtparam) || '' kind = field.kind || defkind case kind when 'text' Inline.new(Vpim.decode_text(field.value), format) when 'uri' Uri.new(field.value_raw, format) when 'binary' Inline.new(field.value, format) else raise InvalidEncodingError, "Attachment of type #{kind} is not allowed" end end # Extends a String to support some of the same methods as Uri. class Inline < String def initialize(s, format) #:nodoc: @format = format super(s) end # Return an IO object for the inline data. See +stringio+ for more # information. def to_io StringIO.new(self) end # The format of the inline data. # See Attachment. attr_reader :format end # Encapsulates a URI and implements some methods of String. class Uri def initialize(uri, format) #:nodoc: @uri = uri @format = format end # The URI value. attr_reader :uri # The format of the data referred to by the URI. # See Attachment. attr_reader :format # Return an IO object from opening the URI. See +open-uri+ for more # information. def to_io open(@uri) end # Return the String from reading the IO object to end-of-data. def to_s to_io.read(nil) end def inspect #:nodoc: s = "<#{self.class.to_s}: #{uri.inspect}>" s << ", #{@format.inspect}" if @format s end end end end vpim-0.695/lib/vpim/date.rb0000644000076500000240000001646311152674120014147 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require 'date' # Extensions to the standard library Date. class Date TIME_START = Date.new(1970, 1, 1) SECS_PER_DAY = 24 * 60 * 60 # Converts this object to a Time object, or throws an ArgumentError if # conversion is not possible because it is before the start of epoch. def vpim_to_time raise ArgumentError, 'date is before the start of system time' if self < TIME_START days = self - TIME_START Time.at((days * SECS_PER_DAY).to_i) end # If wday responds to to_str, convert it to the wday number by searching for # a wday that matches, using as many characters as are in wday to do the # comparison. wday must be 2 or more characters long in order to be a unique # match, other than that, "mo", "Mon", and "MonDay" are all valid strings # for wday 1. # # This method can be called on a valid wday, and it will return it. Perhaps # it should be called by default inside the Date#new*() methods so that # non-integer wday arguments can be used? Perhaps a similar method should # exist for months? But with months, we all know January is 1, who can # remember where Date chooses to start its wday count! # # Examples: # Date.bywday(2004, 2, Date.str2wday('TU')) => the first Tuesday in # February # Date.bywday(2004, 2, Date.str2wday(2)) => the same day, but notice # that a valid wday integer can be passed right through. # def Date.str2wday(wdaystr) return wdaystr unless wdaystr.respond_to? :to_str str = wdaystr.to_str.upcase if str.length < 2 raise ArgumentError, 'wday #{wday} is not long enough to be a unique weekday name' end wday = Date::DAYNAMES.map { |n| n.slice(0, str.length).upcase }.index(str) return wday if wday raise ArgumentError, 'wday #{wdaystr} was not a recognizable weekday name' end # Create a new Date object for the date specified by year +year+, month # +mon+, and day-of-the-week +wday+. # # The nth, +n+, occurrence of +wday+ within the period will be generated # (+n+ defaults to 1). If +n+ is positive, the nth occurrence from the # beginning of the period will be returned, if negative, the nth occurrence # from the end of the period will be returned. # # The period is a year, unless +month+ is non-nil, in which case it is just # that month. # # Examples: # - Date.bywday(2004, nil, 1, 9) => the ninth Sunday of 2004 # - Date.bywday(2004, nil, 1) => the first Sunday of 2004 # - Date.bywday(2004, nil, 1, -2) => the second last Sunday of 2004 # - Date.bywday(2004, 12, 1) => the first sunday in the 12th month of 2004 # - Date.bywday(2004, 2, 2, -1) => last Tuesday in the 2nd month in 2004 # - Date.bywday(2004, -2, 3, -2) => second last Wednesday in the second last month of 2004 # # Compare this to Date.new, which allows a Date to be created by # day-of-the-month, mday, to Date.ordinal, which allows a Date to be created by # day-of-the-year, yday, and to Date.commercial, which allows a Date to be created # by day-of-the-week, but within a specific week. def Date.bywday(year, mon, wday, n = 1, sg=Date::ITALY) # Normalize mon to 1-12. if mon if mon > 12 || mon == 0 || mon < -12 raise ArgumentError, "mon #{mon} must be 1-12 or negative 1-12" end if mon < 0 mon = 13 + mon end end if wday < 0 || wday > 6 raise ArgumentError, 'wday must be in range 0-6, or a weekday name' end # Determine direction of indexing. inc = n <=> 0 if inc == 0 raise ArgumentError, 'n must be greater or less than zero' end # if !mon, n is index into year, but direction of search is determined by # sign of n d = Date.new(year, mon ? mon : inc, inc, sg) while d.wday != wday d += inc end # Now we have found the first/last day with the correct wday, search # for nth occurrence, by jumping by n.abs-1 weeks forward or backward. d += 7 * (n.abs - 1) * inc if d.year != year raise ArgumentError, 'n is out of bounds of year' end if mon && d.mon != mon raise ArgumentError, 'n is out of bounds of month' end d end # Return the first day of the week for the specified date. Commercial weeks # start on Monday, but the weekstart can be specified (as 0-6, where 0 is # sunday, or in formate of Date.str2day). def Date.weekstart(year, mon, day, weekstart="MO") wkst = Date.str2wday(weekstart) d = Date.new(year, mon, day) until d.wday == wkst d = d - 1 end d end end # DateGen generates arrays of dates matching simple criteria. class DateGen # Generate an array of a week's dates, where week is specified by year, mon, # day, and the weekstart (the day-of-week that is considered the "first" day # of that week, 0-6, where 0 is sunday). def DateGen.weekofdate(year, mon, day, weekstart) d = Date.weekstart(year, mon, day, weekstart) week = [] 7.times do week << d d = d + 1 end week end # Generate an array of dates on +wday+ (the day-of-week, # 0-6, where 0 is Sunday). # # If +n+ is specified, only the nth occurrence of +wday+ within the period # will be generated. If +n+ is positive, the nth occurrence from the # beginning of the period will be returned, if negative, the nth occurrence # from the end of the period will be returned. # # The period is a year, unless +month+ is non-nil, in which case it is just # that month. # # Examples: # - DateGen.bywday(2004, nil, 1, 9) => the ninth Sunday in 2004 # - DateGen.bywday(2004, nil, 1) => all Sundays in 2004 # - DateGen.bywday(2004, nil, 1, -2) => second last Sunday in 2004 # - DateGen.bywday(2004, 12, 1) => all sundays in December 2004 # - DateGen.bywday(2004, 2, 2, -1) => last Tuesday in February in 2004 # - DateGen.bywday(2004, -2, 3, -2) => second last Wednesday in November of 2004 # # Compare to Date.bywday(), which allows a single Date to be created with # similar criteria. def DateGen.bywday(year, month, wday, n = nil) seed = Date.bywday(year, month, wday, n ? n : 1) dates = [ seed ] return dates if n succ = seed.clone # Collect all matches until we're out of the year (or month, if specified) loop do succ += 7 break if succ.year != year break if month && succ.month != seed.month dates.push succ end dates.sort! dates end # Generate an array of dates on +mday+ (the day-of-month, 1-31). For months # in which the +mday+ is not present, no date will be generated. # # The period is a year, unless +month+ is non-nil, in which case it is just # that month. # # Compare to Date.new(), which allows a single Date to be created with # similar criteria. def DateGen.bymonthday(year, month, mday) months = month ? [ month ] : 1..12 dates = [ ] months.each do |m| begin dates << Date.new(year, m, mday) rescue ArgumentError # Don't generate dates for invalid combinations (Feb 29, when it's not # a leap year, for example). # # TODO - should we raise when month is out of range, or mday can never # be in range (32)? end end dates end end vpim-0.695/lib/vpim/dirinfo.rb0000644000076500000240000002072511152674120014660 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require 'vpim/enumerator' require 'vpim/field' require 'vpim/rfc2425' require 'vpim/vpim' module Vpim # An RFC 2425 directory info object. # # A directory information object is a sequence of fields. The basic # structure of the object, and the way in which it is broken into fields # is common to all profiles of the directory info type. # # A vCard, for example, is a specialization of a directory info object. # # - [RFC2425] the directory information framework (ftp://ftp.ietf.org/rfc/rfc2425.txt) # # Here's an example of encoding a simple vCard using the low-level APIs: # # card = Vpim::Vcard.create # card << Vpim::DirectoryInfo::Field.create('EMAIL', 'user.name@example.com', 'TYPE' => 'INTERNET' ) # card << Vpim::DirectoryInfo::Field.create('URL', 'http://www.example.com/user' ) # card << Vpim::DirectoryInfo::Field.create('FN', 'User Name' ) # puts card.to_s # # Don't do it like that, use Vpim::Vcard::Maker. class DirectoryInfo include Enumerable private_class_method :new # Initialize a DirectoryInfo object from +fields+. If +profile+ is # specified, check the BEGIN/END fields. def initialize(fields, profile = nil) #:nodoc: if fields.detect { |f| ! f.kind_of? DirectoryInfo::Field } raise ArgumentError, 'fields must be an array of DirectoryInfo::Field objects' end @string = nil # this is used as a flag to indicate that recoding will be necessary @fields = fields check_begin_end(profile) if profile end # Decode +card+ into a DirectoryInfo object. # # +card+ may either be a something that is convertible to a string using # #to_str or an Array of objects that can be joined into a string using # #join("\n"), or an IO object (which will be read to end-of-file). # # The lines in the string may be delimited using IETF (CRLF) or Unix (LF) conventions. # # A DirectoryInfo is mutable, you can add new fields to it, see # Vpim::DirectoryInfo::Field#create() for how to create a new Field. # # TODO: I don't believe this is ever used, maybe I can remove it. def DirectoryInfo.decode(card) #:nodoc: if card.respond_to? :to_str string = card.to_str elsif card.kind_of? Array string = card.join("\n") elsif card.kind_of? IO string = card.read(nil) else raise ArgumentError, "DirectoryInfo cannot be created from a #{card.type}" end fields = Vpim.decode(string) new(fields) end # Create a new DirectoryInfo object. The +fields+ are an optional array of # DirectoryInfo::Field objects to add to the new object, between the # BEGIN/END. If the +profile+ string is not nil, then it is the name of # the directory info profile, and the BEGIN:+profile+/END:+profile+ fields # will be added. # # A DirectoryInfo is mutable, you can add new fields to it using #push(), # and see Field#create(). def DirectoryInfo.create(fields = [], profile = nil) if profile p = profile.to_str f = [ Field.create('BEGIN', p) ] f.concat fields f.push Field.create('END', p) fields = f end new(fields, profile) end # The first field named +name+, or nil if no # match is found. def field(name) enum_by_name(name).each { |f| return f } nil end # The value of the first field named +name+, or nil if no # match is found. def [](name) enum_by_name(name).each { |f| return f.value if f.value != ''} enum_by_name(name).each { |f| return f.value } nil end # An array of all the values of fields named +name+, converted to text # (using Field#to_text()). # # TODO - call this #texts(), as in the plural? def text(name) accum = [] each do |f| if f.name? name accum << f.to_text end end accum end # Array of all the Field#group()s. def groups @fields.collect { |f| f.group } .compact.uniq end # All fields, frozen. def fields #:nodoc: @fields.dup.freeze end # Yields for each Field for which +cond+.call(field) is true. The # (default) +cond+ of nil is considered true for all fields, so # this acts like a normal #each() when called with no arguments. def each(cond = nil) # :yields: Field @fields.each do |field| if(cond == nil || cond.call(field)) yield field end end self end # Returns an Enumerator for each Field for which #name?(+name+) is true. # # An Enumerator supports all the methods of Enumerable, so it allows iteration, # collection, mapping, etc. # # Examples: # # Print all the nicknames in a card: # # card.enum_by_name('NICKNAME') { |f| puts f.value } # # Print an Array of the preferred email addresses in the card: # # pref_emails = card.enum_by_name('EMAIL').select { |f| f.pref? } def enum_by_name(name) Enumerator.new(self, Proc.new { |field| field.name?(name) }) end # Returns an Enumerator for each Field for which #group?(+group+) is true. # # For example, to print all the fields, sorted by group, you could do: # # card.groups.sort.each do |group| # card.enum_by_group(group).each do |field| # puts "#{group} -> #{field.name}" # end # end # # or to get an array of all the fields in group 'AGROUP', you could do: # # card.enum_by_group('AGROUP').to_a def enum_by_group(group) Enumerator.new(self, Proc.new { |field| field.group?(group) }) end # Returns an Enumerator for each Field for which +cond+.call(field) is true. def enum_by_cond(cond) Enumerator.new(self, cond ) end # Force card to be reencoded from the fields. def dirty #:nodoc: #string = nil end # Append +field+ to the fields. Note that it won't be literally appended # to the fields, it will be inserted before the closing END field. def push(field) dirty @fields[-1,0] = field self end alias << push # Push +field+ onto the fields, unless there is already a field # with this name. def push_unique(field) push(field) unless @fields.detect { |f| f.name? field.name } self end # Append +field+ to the end of all the fields. This isn't usually what you # want to do, usually a DirectoryInfo's first and last fields are a # BEGIN/END pair, see #push(). def push_end(field) @fields << field self end # Delete +field+. # # Warning: You can't delete BEGIN: or END: fields, but other # profile-specific fields can be deleted, including mandatory ones. For # vCards in particular, in order to avoid destroying them, I suggest # creating a new Vcard, and copying over all the fields that you still # want, rather than using #delete. This is easy with Vcard::Maker#copy, see # the Vcard::Maker examples. def delete(field) case when field.name?('BEGIN'), field.name?('END') raise ArgumentError, 'Cannot delete BEGIN or END fields.' else @fields.delete field end self end # The string encoding of the DirectoryInfo. See Field#encode for information # about the width parameter. def encode(width=nil) unless @string @string = @fields.collect { |f| f.encode(width) } . join "" end @string end alias to_s encode # Check that the DirectoryInfo object is correctly delimited by a BEGIN # and END, that their profile values match, and if +profile+ is specified, that # they are the specified profile. def check_begin_end(profile=nil) #:nodoc: unless @fields.first raise "No fields to check" end unless @fields.first.name? 'BEGIN' raise "Needs BEGIN, found: #{@fields.first.encode nil}" end unless @fields.last.name? 'END' raise "Needs END, found: #{@fields.last.encode nil}" end unless @fields.last.value? @fields.first.value raise "BEGIN/END mismatch: (#{@fields.first.value} != #{@fields.last.value}" end if profile if ! @fields.first.value? profile raise "Mismatched profile" end end true end end end vpim-0.695/lib/vpim/duration.rb0000644000076500000240000000412211152674120015044 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end module Vpim class Duration SECS_HOUR = 60 * 60 SECS_DAY = 24 * SECS_HOUR MINS_HOUR = 60 # Initialize from a number of seconds. def initialize(secs) @secs = secs end def Duration.secs(secs) Duration.new(secs) end def Duration.mins(mins) Duration.new(mins * 60) end def Duration.hours(hours) Duration.new(hours * SECS_HOUR) end def Duration.days(days) Duration.new(days * SECS_DAY) end def secs @secs end def mins (@secs/60).to_i end def hours (@secs/SECS_HOUR).to_i end def days (@secs/SECS_DAY).to_i end def weeks (days/7).to_i end def by_hours [ hours, mins % MINS_HOUR, secs % 60] end def by_days [ days, hours % 24, mins % MINS_HOUR, secs % 60] end def to_a by_days end def to_s Duration.as_str(self.to_a) end def Duration.as_str(arr) s = "" case arr.length when 4 if arr[0] > 0 s << "#{arr[0]} days" end if arr[1] > 0 if s.length > 0 s << ', ' end s << "#{arr[1]} hours" end if arr[2] > 0 if s.length > 0 s << ', ' end s << "#{arr[2]} mins" end if arr[3] > 0 if s.length > 0 s << ', ' end s << "#{arr[3]} secs" end when 3 if arr[0] > 0 s << "#{arr[0]} hours" end if arr[1] > 0 if s.length > 0 s << ', ' end s << "#{arr[1]} mins" end if arr[2] > 0 if s.length > 0 s << ', ' end s << "#{arr[2]} secs" end end s end end end vpim-0.695/lib/vpim/enumerator.rb0000644000076500000240000000145711152674120015410 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require "enumerator" module Vpim # This is a way for an object to have multiple ways of being enumerated via # argument to it's #each() method. An Enumerator mixes in Enumerable, so the # standard APIs such as Enumerable#map(), Enumerable#to_a(), and # Enumerable#find_all() can be used on it. # # TODO since 1.8, this is part of the standard library, I should rewrite vPim # so this can be removed. class Enumerator include Enumerable def initialize(obj, *args) @obj = obj @args = args end def each(&block) @obj.each(*@args, &block) end end end vpim-0.695/lib/vpim/field.rb0000644000076500000240000004462011152674120014311 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require 'vpim/rfc2425' require 'vpim/vpim' require 'date' module Vpim class DirectoryInfo # A field in a directory info object. class Field # TODO # - Field should know which param values and field values are # case-insensitive, configurably, so it can down case them # - perhaps should have pvalue_set/del/add, perhaps case-insensitive, or # pvalue_iset/idel/iadd, where set sets them all, add adds if not present, # and del deletes any that are present # - I really, really, need a case-insensitive string... # - should allow nil as a field value, its not the same as '', if there is # more than one pvalue, the empty string will show up. This isn't strictly # disallowed, but its odd. Should also strip empty strings on decoding, if # I don't already. private_class_method :new def Field.create_array(fields) case fields when Hash fields.map do |name,value| DirectoryInfo::Field.create( name, value ) end else fields.to_ary end end # Encode a field. def Field.encode0(group, name, params={}, value='') # :nodoc: line = "" # A reminder of the line format: # [.];=,: if group line << group << '.' end line << name params.each do |pname, pvalues| unless pvalues.respond_to? :to_ary pvalues = [ pvalues ] end line << ';' << pname << '=' sep = "" # set to ',' after one pvalue has been appended pvalues.each do |pvalue| # check if we need to do any encoding if Vpim::Methods.casecmp?(pname, 'ENCODING') && pvalue == :b64 pvalue = 'B' # the RFC definition of the base64 param value value = [ value.to_str ].pack('m').gsub("\n", '') end line << sep << pvalue sep =","; end end line << ':' line << Field.value_str(value) line end def Field.value_str(value) # :nodoc: line = '' case value when Date line << Vpim.encode_date(value) when Time #, DateTime line << Vpim.encode_date_time(value) when Array line << value.map { |v| Field.value_str(v) }.join(';') when Symbol line << value else # FIXME - somewhere along here, values with special chars need escaping... line << value.to_str end line end # Decode a field. def Field.decode0(atline) # :nodoc: unless atline =~ %r{#{Bnf::LINE}}i raise Vpim::InvalidEncodingError, atline end atgroup = $1.upcase atname = $2.upcase paramslist = $3 atvalue = $~[-1] # I've seen space that shouldn't be there, as in "BEGIN:VCARD ", so # strip it. I'm not absolutely sure this is allowed... it certainly # breaks round-trip encoding. atvalue.strip! if atgroup.length > 0 atgroup.chomp!('.') else atgroup = nil end atparams = {} # Collect the params, if any. if paramslist.size > 1 # v3.0 and v2.1 params paramslist.scan( %r{#{Bnf::PARAM}}i ) do # param names are case-insensitive, and multi-valued name = $1.upcase params = $3 # v2.1 params have no '=' sign, figure out what kind of param it # is (either its a known encoding, or we treat it as a 'TYPE' # param). if $2 == "" params = $1 case $1 when /quoted-printable/i name = 'ENCODING' when /base64/i name = 'ENCODING' else name = 'TYPE' end end # TODO - In ruby1.8 I can give an initial value to the atparams # hash values instead of this. unless atparams.key? name atparams[name] = [] end params.scan( %r{#{Bnf::PVALUE}} ) do atparams[name] << ($1 || $2) end end end [ atgroup, atname, atparams, atvalue ] end def initialize(line) # :nodoc: @line = line.to_str @group, @name, @params, @value = Field.decode0(@line) @params.each do |pname,pvalues| pvalues.freeze end self end # Create a field by decoding +line+, a String which must already be # unfolded. Decoded fields are frozen, but see #copy(). def Field.decode(line) new(line).freeze end # Create a field with name +name+ (a String), value +value+ (see below), # and optional parameters, +params+. +params+ is a hash of the parameter # name (a String) to either a single string or symbol, or an array of # strings and symbols (parameters can be multi-valued). # # If 'ENCODING' => :b64 is specified as a parameter, the value will be # base-64 encoded. If it's already base-64 encoded, then use String # values ('ENCODING' => 'B'), and no further encoding will be done by # this routine. # # Currently handled value types are: # - Time, encoded as a date-time value # - Date, encoded as a date value # - String, encoded directly # - Array of String, concatentated with ';' between them. # # TODO - need a way to encode String values as TEXT, at least optionally, # so as to escape special chars, etc. def Field.create(name, value="", params={}) line = Field.encode0(nil, name, params, value) begin new(line) rescue Vpim::InvalidEncodingError => e raise ArgumentError, e.to_s end end # Create a copy of Field. If the original Field was frozen, this one # won't be. def copy Marshal.load(Marshal.dump(self)) end # The String encoding of the Field. The String will be wrapped to a # maximum line width of +width+, where +0+ means no wrapping, and nil is # to accept the default wrapping (75, recommended by RFC2425). # # Note: AddressBook.app 3.0.3 neither understands to unwrap lines when it # imports vCards (it treats them as raw new-line characters), nor wraps # long lines on export. This is mostly a cosmetic problem, but wrapping # can be disabled by setting width to +0+, if desired. # # FIXME - breaks round-trip encoding, need to change this to not wrap # fields that are already wrapped. def encode(width=nil) width = 75 unless width l = @line # Wrap to width, unless width is zero. if width > 0 l = l.gsub(/.{#{width},#{width}}/) { |m| m + "\n " } end # Make sure it's terminated with no more than a single NL. l.gsub(/\s*\z/, '') + "\n" end alias to_s encode # The name. def name @name end # The group, if present, or nil if not present. def group @group end # An Array of all the param names. def pnames @params.keys end # FIXME - remove my own uses of #params alias params pnames # :nodoc: # The first value of the param +name+, nil if there is no such param, # the param has no value, or the first param value is zero-length. def pvalue(name) v = pvalues( name ) if v v = v.first end if v v = nil unless v.length > 0 end v end # The Array of all values of the param +name+, nil if there is no such # param, [] if the param has no values. If the Field isn't frozen, the # Array is mutable. def pvalues(name) @params[name.upcase] end # FIXME - remove my own uses of #param alias param pvalues # :nodoc: alias [] pvalues # Yield once for each param, +name+ is the parameter name, +value+ is an # array of the parameter values. def each_param(&block) #:yield: name, value if @params @params.each(&block) end end # The decoded value. # # The encoding specified by the #encoding, if any, is stripped. # # Note: Both the RFC 2425 encoding param ("b", meaning base-64) and the # vCard 2.1 encoding params ("base64", "quoted-printable", "8bit", and # "7bit") are supported. # # FIXME: # - should use the VALUE parameter # - should also take a default value type, so it can be converted # if VALUE parameter is not present. def value case encoding when nil, '8BIT', '7BIT' then @value # Hack - if the base64 lines started with 2 SPC chars, which is invalid, # there will be extra spaces in @value. Since no SPC chars show up in # b64 encodings, they can be safely stripped out before unpacking. when 'B', 'BASE64' then @value.gsub(' ', '').unpack('m*').first when 'QUOTED-PRINTABLE' then @value.unpack('M*').first else raise Vpim::InvalidEncodingError, "unrecognized encoding (#{encoding})" end end # Is the #name of this Field +name+? Names are case insensitive. def name?(name) Vpim::Methods.casecmp?(@name, name) end # Is the #group of this field +group+? Group names are case insensitive. # A +group+ of nil matches if the field has no group. def group?(group) Vpim::Methods.casecmp?(@group, group) end # Is the value of this field of type +kind+? RFC2425 allows the type of # a fields value to be encoded in the VALUE parameter. Don't rely on its # presence, they aren't required, and usually aren't bothered with. In # cases where the kind of value might vary (an iCalendar DTSTART can be # either a date or a date-time, for example), you are more likely to see # the kind of value specified explicitly. # # The value types defined by RFC 2425 are: # - uri: # - text: # - date: a list of 1 or more dates # - time: a list of 1 or more times # - date-time: a list of 1 or more date-times # - integer: # - boolean: # - float: def kind?(kind) Vpim::Methods.casecmp?(self.kind == kind) end # Is one of the values of the TYPE parameter of this field +type+? The # type parameter values are case insensitive. False if there is no TYPE # parameter. # # TYPE parameters are used for general categories, such as # distinguishing between an email address used at home or at work. def type?(type) type = type.to_str types = param('TYPE') if types types = types.detect { |t| Vpim::Methods.casecmp?(t, type) } end end # Is this field marked as preferred? A vCard field is preferred if # #type?('PREF'). This method is not necessarily meaningful for # non-vCard profiles. def pref? type? 'PREF' end # Set whether a field is marked as preferred. See #pref? def pref=(ispref) if ispref pvalue_iadd('TYPE', 'PREF') else pvalue_idel('TYPE', 'PREF') end end # Is the value of this field +value+? The check is case insensitive. # FIXME - it shouldn't be insensitive, make a #casevalue? method. def value?(value) Vpim::Methods.casecmp?(@value, value.to_str) end # The value of the ENCODING parameter, if present, or nil if not # present. def encoding e = param('ENCODING') if e if e.length > 1 raise Vpim::InvalidEncodingError, "multi-valued param 'ENCODING' (#{e})" end e = e.first.upcase end e end # The type of the value, as specified by the VALUE parameter, nil if # unspecified. def kind v = param('VALUE') if v if v.size > 1 raise InvalidEncodingError, "multi-valued param 'VALUE' (#{values})" end v = v.first.downcase end v end # The value as an array of Time objects (all times and dates in # RFC2425 are lists, even where it might not make sense, such as a # birthday). The time will be UTC if marked as so (with a timezone of # "Z"), and in localtime otherwise. # # TODO - support timezone offsets # # TODO - if year is before 1970, this won't work... but some people # are generating calendars saying Canada Day started in 1753! # That's just wrong! So, what to do? I add a message # saying what the year is that breaks, so they at least know that # its ridiculous! I think I need my own DateTime variant. def to_time begin Vpim.decode_date_time_list(value).collect do |d| # We get [ year, month, day, hour, min, sec, usec, tz ] begin if(d.pop == "Z") Time.gm(*d) else Time.local(*d) end rescue ArgumentError => e raise Vpim::InvalidEncodingError, "Time.gm(#{d.join(', ')}) failed with #{e.message}" end end rescue Vpim::InvalidEncodingError Vpim.decode_date_list(value).collect do |d| # We get [ year, month, day ] begin Time.gm(*d) rescue ArgumentError => e raise Vpim::InvalidEncodingError, "Time.gm(#{d.join(', ')}) failed with #{e.message}" end end end end # The value as an array of Date objects (all times and dates in # RFC2425 are lists, even where it might not make sense, such as a # birthday). # # The field value may be a list of either DATE or DATE-TIME values, # decoding is tried first as a DATE-TIME, then as a DATE, if neither # works an InvalidEncodingError will be raised. def to_date begin Vpim.decode_date_time_list(value).collect do |d| # We get [ year, month, day, hour, min, sec, usec, tz ] Date.new(d[0], d[1], d[2]) end rescue Vpim::InvalidEncodingError Vpim.decode_date_list(value).collect do |d| # We get [ year, month, day ] Date.new(*d) end end end # The value as text. Text can have escaped newlines, commas, and escape # characters, this method will strip them, if present. # # In theory, #value could also do this, but it would need to know that # the value is of type 'TEXT', and often for text values the 'VALUE' # parameter is not present, so knowledge of the expected type of the # field is required from the decoder. def to_text Vpim.decode_text(value) end # The undecoded value, see +value+. def value_raw @value end # TODO def pretty_print() ... # Set the group of this field to +group+. def group=(group) mutate(group, @name, @params, @value) group end # Set the value of this field to +value+. Valid values are as in # Field.create(). def value=(value) mutate(@group, @name, @params, value) value end # Convert +value+ to text, then assign. # # TODO - unimplemented def text=(text) end # Set a the param +pname+'s value to +pvalue+, replacing any value it # currently has. See Field.create() for a description of +pvalue+. # # Example: # if field['TYPE'] # field['TYPE'] << 'HOME' # else # field['TYPE'] = [ 'HOME' ] # end # # TODO - this could be an alias to #pvalue_set def []=(pname,pvalue) unless pvalue.respond_to?(:to_ary) pvalue = [ pvalue ] end h = @params.dup h[pname.upcase] = pvalue mutate(@group, @name, h, @value) pvalue end # Add +pvalue+ to the param +pname+'s value. The values are treated as a # set so duplicate values won't occur, and String values are case # insensitive. See Field.create() for a description of +pvalue+. def pvalue_iadd(pname, pvalue) pname = pname.upcase # Get a uniq set, where strings are compared case-insensitively. values = [ pvalue, @params[pname] ].flatten.compact values = values.collect do |v| if v.respond_to? :to_str v = v.to_str.upcase end v end values.uniq! h = @params.dup h[pname] = values mutate(@group, @name, h, @value) values end # Delete +pvalue+ from the param +pname+'s value. The values are treated # as a set so duplicate values won't occur, and String values are case # insensitive. +pvalue+ must be a single String or Symbol. def pvalue_idel(pname, pvalue) pname = pname.upcase if pvalue.respond_to? :to_str pvalue = pvalue.to_str.downcase end # Get a uniq set, where strings are compared case-insensitively. values = [ nil, @params[pname] ].flatten.compact values = values.collect do |v| if v.respond_to? :to_str v = v.to_str.downcase end v end values.uniq! values.delete pvalue h = @params.dup h[pname] = values mutate(@group, @name, h, @value) values end # FIXME - should change this so it doesn't assign to @line here, so @line # is used to preserve original encoding. That way, #encode can only wrap # new fields, not old fields. def mutate(g, n, p, v) #:nodoc: line = Field.encode0(g, n, p, v) begin @group, @name, @params, @value = Field.decode0(line) @line = line rescue Vpim::InvalidEncodingError => e raise ArgumentError, e.to_s end self end private :mutate end end end vpim-0.695/lib/vpim/icalendar.rb0000644000076500000240000002650511152674120015152 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require "enumerator" require 'vpim/rfc2425' require 'vpim/dirinfo' require 'vpim/rrule' require 'vpim/vevent' require 'vpim/vtodo' require 'vpim/vjournal' require 'vpim/vpim' module Vpim # An iCalendar. # # A Calendar is some meta-information followed by a sequence of components. # # Defined components are Event, Todo, Freebusy, Journal, and Timezone, each # of which are represented by their own class, though they share many # properties in common. For example, Event and Todo may both contain # multiple Alarm components. # # = Reference # # The iCalendar format is specified by a series of IETF documents: # # - link:rfc2445.txt: Internet Calendaring and Scheduling Core Object Specification # - link:rfc2446.txt: iCalendar Transport-Independent Interoperability Protocol # (iTIP) Scheduling Events, BusyTime, To-dos and Journal Entries # - link:rfc2447.txt: iCalendar Message-Based Interoperability Protocol # # = iCalendar and vCalendar # # iCalendar files have VERSION:2.0 and vCalendar have VERSION:1.0. iCalendar # (RFC 2445) is based on vCalendar, but but is not very compatible. While # much appears to be similar, the recurrence rule syntax is completely # different. # # iCalendars are usually transmitted in files with .ics # extensions. class Icalendar # FIXME do NOT do this! include Vpim # Regular expression strings for the EBNF of RFC 2445 module Bnf #:nodoc: # dur-value = ["+" / "-"] "P" [ 1*DIGIT "W" ] [ 1*DIGIT "D" ] [ "T" [ 1*DIGIT "H" ] [ 1*DIGIT "M" ] [ 1*DIGIT "S" ] ] DURATION = '([-+])?P(\d+W)?(\d+D)?T?(\d+H)?(\d+M)?(\d+S)?' end private_class_method :new # Create a new Icalendar object from +fields+, an array of # DirectoryInfo::Field objects. # # When decoding Calendar data, you would usually use Icalendar.decode(), # which decodes the data into the field arrays, and calls this method # for each Calendar it finds. def initialize(fields) #:nodoc: # seperate into the outer-level fields, and the arrays of component # fields outer, inner = Vpim.outer_inner(fields) # Make a dirinfo out of outer, and check its an iCalendar @properties = DirectoryInfo.create(outer) @properties.check_begin_end('VCALENDAR') @components = [] # could use #constants instead of this factory = { 'VEVENT' => Vevent, 'VTODO' => Vtodo, 'VJOURNAL' => Vjournal, # TODO - VTIMEZONE } inner.each do |component| name = component.first.value if klass = factory[name] @components << klass.new(component) end end end # Add an event to this calendar. # # Yields an event maker, Icalendar::Vevent::Maker. def add_event(&block) #:yield:event push Vevent::Maker.make( &block ) end # TODO add_todo, add_journal =begin TODO # Allows customization of calendar creation. class Maker def initialize #:nodoc: @prodid = Vpim::PRODID end attr :prodid end =end # The producer ID defaults to Vpim::PRODID but you can set it to something # specific to your application. def Icalendar.create2(producer = Vpim::PRODID) #:yield: self # FIXME - make the primary API di = DirectoryInfo.create( [ DirectoryInfo::Field.create('VERSION', '2.0') ], 'VCALENDAR' ) di.push_unique DirectoryInfo::Field.create('PRODID', producer.to_str) di.push_unique DirectoryInfo::Field.create('CALSCALE', "Gregorian") cal = new(di.to_a) if block_given? yield cal end cal end # Create a new Icalendar object with the minimal set of fields for a valid # Calendar. If specified, +fields+ must be an array of # DirectoryInfo::Field objects to add. They can override the the default # Calendar fields, so, for example, this can be used to set a custom PRODID field. def Icalendar.create(fields=[]) di = DirectoryInfo.create( [ DirectoryInfo::Field.create('VERSION', '2.0') ], 'VCALENDAR' ) DirectoryInfo::Field.create_array(fields).each { |f| di.push_unique f } di.push_unique DirectoryInfo::Field.create('PRODID', Vpim::PRODID) di.push_unique DirectoryInfo::Field.create('CALSCALE', "Gregorian") new(di.to_a) end # Create a new Icalendar object with a protocol method of REPLY. # # Meeting requests, and such, are Calendar containers with a protocol # method of REQUEST, and contains some number of Events, Todos, etc., # that may need replying to. In order to reply to any of these components # of a request, you must first build a Calendar object to hold your reply # components. # # This method builds the reply Calendar, you then will add to it replies # to the specific components of the request Calendar that you are replying # to. If you have any particular fields that you want to be in the # Calendar, other than the defaults, then can be supplied as +fields+, an # array of Field objects. def Icalendar.create_reply(fields=[]) fields << DirectoryInfo::Field.create('METHOD', 'REPLY') Icalendar.create(fields) end # Used during encoding. def fields # :nodoc: f = @properties.to_a last = f.pop # Use of #each means we won't encode components in our View, but also # that we won't encode timezones... but we don't decode/support timezones # anyhow, so fix later. each { |c| f << c.fields } f.push last end # Encode the Calendar as a string. The width is the maximum width of the # encoded lines, it can be specified, but is better left to the default. def encode(width=nil) # We concatenate the fields of all objects, create a DirInfo, then # encode it. di = DirectoryInfo.create(self.fields.flatten) di.encode(width) end alias to_s encode # Push a calendar component onto the calendar. def push(component) case component when Vevent, Vtodo, Vjournal @components << component else raise ArgumentError, "can't add a #{component.type} to a calendar" end self end alias :<< :push # Check if the protocol method is +method+ def protocol?(method) Vpim::Methods.casecmp?(protocol, method) end def Icalendar.decode_duration(str) #:nodoc: unless match = %r{\s*#{Bnf::DURATION}\s*}.match(str) raise InvalidEncodingError, "duration not valid (#{str})" end dur = 0 # Remember: match[0] is the whole match string, match[1] is $1, etc. # Week if match[2] dur = match[2].to_i end # Days dur *= 7 if match[3] dur += match[3].to_i end # Hours dur *= 24 if match[4] dur += match[4].to_i end # Minutes dur *= 60 if match[5] dur += match[5].to_i end # Seconds dur *= 60 if match[6] dur += match[6].to_i end if match[1] && match[1] == '-' dur = -dur end dur end # Decode iCalendar data into an array of Icalendar objects. # # Since iCalendars are self-delimited (by a BEGIN:VCALENDAR and an # END:VCALENDAR), multiple iCalendars can be concatenated into a single # file. # # cal must be String or IO, or implement #each by returning # each line in the input as those classes do. def Icalendar.decode(cal, e = nil) entities = Vpim.expand(Vpim.decode(cal)) # Since all iCalendars must have a begin/end, the top-level should # consist entirely of entities/arrays, even if its a single iCalendar. if entities.detect { |e| ! e.kind_of? Array } raise "Not a valid iCalendar" end calendars = [] entities.each do |e| calendars << new(e) end calendars end # The iCalendar version multiplied by 10 as an Integer. iCalendar must have # a version of 20, and vCalendar must have a version of 10. def version v = @properties['VERSION'] unless v raise InvalidEncodingError, "Invalid calendar, no version field!" end v = v.to_f * 10 v = v.to_i end # The value of the PRODID field, an unstructured string meant to # identify the software which encoded the Calendar data. def producer #f = @properties.field('PRODID') #f && f.to_text @properties.text('PRODID').first end # The value of the METHOD field. Protocol methods are used when iCalendars # are exchanged in a calendar messaging system, such as iTIP or iMIP. When # METHOD is not specified, the Calendar object is merely being used to # transport a snapshot of some calendar information; without the intention # of conveying a scheduling semantic. # # Note that this method can't be called +method+, thats already a method of # Object. def protocol m = @properties['METHOD'] m ? m.upcase : m end # The value of the CALSCALE: property, or "GREGORIAN" if CALSCALE: is not # present. # # This is of academic interest only. There aren't any other calendar scales # defined, and given that its hard enough just dealing with Gregorian # calendars, there probably won't be. def calscale (@properties['CALSCALE'] || 'GREGORIAN').upcase end # The array of all supported calendar components. If a class is provided, # return only the components of that class. # # If a block is provided, yield the components instead of returning them. # # Examples: # calendar.components(Vpim::Icalendar::Vevent) # => array of all calendar components # # calendar.components(Vpim::Icalendar::Vtodo) {|c| c... } # => yield all todo components # # calendar.components {|c| c... } # => yield all components # # Note - use of this is mildly deprecated in favour of #each, #events, # #todos, #journals because those won't return timezones, and will return # Enumerators if called without a block. def components(klass=Object) #:yields:component klass ||= Object unless block_given? return @components.select{|c| klass === c}.freeze end @components.each do |c| if klass === c yield c end end self end include Enumerable # Enumerate the top-level calendar components. Yields them if a block # is provided, otherwise returns an Enumerator. # # This skips components that are only internally meaningful to iCalendar, # such as timezone definitions. def each(klass=nil, &block) # :yield: component unless block return Enumerable::Enumerator.new(self, :each, klass) end components(klass, &block) end # Short-hand for #each(Icalendar::Vevent). def events(&block) #:yield: Vevent each(Icalendar::Vevent, &block) end # Short-hand for #each(Icalendar::Vtodo). def todos(&block) #:yield: Vtodo each(Icalendar::Vtodo, &block) end # Short-hand for #each(Icalendar::Vjournal). def journals(&block) #:yield: Vjournal each(Icalendar::Vjournal, &block) end end end vpim-0.695/lib/vpim/maker/0000755000076500000240000000000011152674120013772 5ustar samstaffvpim-0.695/lib/vpim/maker/vcard.rb0000644000076500000240000000053611152674120015422 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require 'vpim/vcard' module Vpim module Maker #:nodoc:backwards compat Vcard = Vpim::Vcard::Maker #:nodoc:backwards compat end end vpim-0.695/lib/vpim/property/0000755000076500000240000000000011152674120014557 5ustar samstaffvpim-0.695/lib/vpim/property/base.rb0000644000076500000240000001232511152674120016021 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end module Vpim class Icalendar module Set #:nodoc: module Util #:nodoc: # TODO - rename module to Private? def rm_all(name) rm = @comp.properties.select { |f| f.name? name } rm.each { |f| @comp.properties.delete(f) } end def set_token(name, allowed, default, value) #:nodoc: value = value.to_str unless allowed.include?(value) raise Vpim::Unencodeable, "Invalid #{name} value '#{value}'" end rm_all(name) unless value == default @comp.properties.push Vpim::DirectoryInfo::Field.create(name, value) end end def field_create(name, value, default_value_type = nil, value_type = nil, params = {}) if value_type && value_type != default_value_type params['VALUE'] = value_type end Vpim::DirectoryInfo::Field.create(name, value, params) end def set_date_or_datetime(name, default, value) f = nil case value when Date f = field_create(name, Vpim.encode_date(value), default, 'DATE') when Time f = field_create(name, Vpim.encode_date_time(value), default, 'DATE-TIME') else raise Vpim::Unencodeable, "Invalid #{name} value #{value.inspect}" end rm_all(name) @comp.properties.push(f) end def set_datetime(name, value) f = field_create(name, Vpim.encode_date_time(value)) rm_all(name) @comp.properties.push(f) end def set_text(name, value) f = field_create(name, Vpim.encode_text(value)) rm_all(name) @comp.properties.push(f) end def set_text_list(name, value) f = field_create(name, Vpim.encode_text_list(value)) rm_all(name) @comp.properties.push(f) end def set_integer(name, value) value = value.to_int.to_s f = field_create(name, value) rm_all(name) @comp.properties.push(f) end def add_address(name, value) f = value.encode(name) @comp.properties.push(f) end def set_address(name, value) rm_all(name) add_address(name, value) end end end module Property #:nodoc: # FIXME - rename Base to Util module Base #:nodoc: # Value of first property with name +name+ def propvalue(name) #:nodoc: prop = @properties.detect { |f| f.name? name } if prop prop = prop.value end prop end # Array of values of all properties with name +name+ def propvaluearray(name) #:nodoc: @properties.select{ |f| f.name? name }.map{ |p| p.value } end def propinteger(name) #:nodoc: prop = @properties.detect { |f| f.name? name } if prop prop = Vpim.decode_integer(prop.value) end prop end def proptoken(name, allowed, default_token = nil) #:nodoc: prop = propvalue(name) if prop prop = prop.to_str.upcase unless allowed.include?(prop) raise Vpim::InvalidEncodingError, "Invalid #{name} value '#{prop}'" end else prop = default_token end prop end # Value as DATE-TIME or DATE of object of first property with name +name+ def proptime(name) #:nodoc: prop = @properties.detect { |f| f.name? name } if prop prop = prop.to_time.first end prop end # Value as TEXT of first property with name +name+ def proptext(name) #:nodoc: prop = @properties.detect { |f| f.name? name } if prop prop = prop.to_text end prop end # Array of values as TEXT of all properties with name +name+ def proptextarray(name) #:nodoc: @properties.select{ |f| f.name? name }.map{ |p| p.to_text } end # Array of values as TEXT list of all properties with name +name+ def proptextlistarray(name) #:nodoc: @properties.select{ |f| f.name? name }.map{ |p| Vpim.decode_text_list(p.value_raw) }.flatten end # Duration has "almost" the same definition for Event and Todo def propduration(endfield) dur = @properties.field 'DURATION' dte = @properties.field endfield if !dur return nil unless dte b = dtstart e = send(endfield.downcase.to_sym) return (e - b).to_i end Icalendar.decode_duration(dur.value_raw) end def propend(endfield) dte = @properties.field endfield.to_s.upcase if dte dte.to_time.first elsif duration dtstart + duration else nil end end end end end end vpim-0.695/lib/vpim/property/common.rb0000644000076500000240000002265411152674120016405 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require 'vpim/address' require 'vpim/attachment' module Vpim class Icalendar module Property # Properties common to Vevent, Vtodo, and Vjournal. module Common # This property defines the access classification for a calendar # component. # # An access classification is only one component of the general # security system within a calendar application. It provides a method # of capturing the scope of the access the calendar owner intends for # information within an individual calendar entry. The access # classification of an individual iCalendar component is useful when # measured along with the other security components of a calendar # system (e.g., calendar user authentication, authorization, access # rights, access role, etc.). Hence, the semantics of the individual # access classifications cannot be completely defined by this memo # alone. Additionally, due to the "blind" nature of most exchange # processes using this memo, these access classifications cannot serve # as an enforcement statement for a system receiving an iCalendar # object. Rather, they provide a method for capturing the intention of # the calendar owner for the access to the calendar component. # # Property Name: CLASS # # Property Value: one of "PUBLIC", "PRIVATE", "CONFIDENTIAL", default # is "PUBLIC" if no CLASS property is found. def access_class proptoken 'CLASS', ["PUBLIC", "PRIVATE", "CONFIDENTIAL"], "PUBLIC" end def created proptime 'CREATED' end # Description of the calendar component, or nil if there is no # description. def description proptext 'DESCRIPTION' end # Revision sequence number of the calendar component, or nil if there # is no SEQUENCE; property. def sequence propinteger 'SEQUENCE' end # The time stamp for this calendar component. def dtstamp proptime 'DTSTAMP' end # The start time for this calendar component. def dtstart proptime 'DTSTART' end def lastmod proptime 'LAST-MODIFIED' end # Return the event organizer, an object of Icalendar::Address (or nil if # there is no ORGANIZER field). def organizer organizer = @properties.field('ORGANIZER') if organizer organizer = Icalendar::Address.decode(organizer) end organizer.freeze end =begin recurid seq =end # Status values are not rejected during decoding. However, if the # status is requested, and it's value is not one of the defined # allowable values, an exception is raised. def status case self when Vpim::Icalendar::Vevent proptoken 'STATUS', ['TENTATIVE', 'CONFIRMED', 'CANCELLED'] when Vpim::Icalendar::Vtodo proptoken 'STATUS', ['NEEDS-ACTION', 'COMPLETED', 'IN-PROCESS', 'CANCELLED'] when Vpim::Icalendar::Vevent proptoken 'STATUS', ['DRAFT', 'FINAL', 'CANCELLED'] end end # TODO - def status? ... # TODO - def status= ... # Summary description of the calendar component, or nil if there is no # SUMMARY property. def summary proptext 'SUMMARY' end # The unique identifier of this calendar component, a string. def uid proptext 'UID' end def url propvalue 'URL' end # Return an array of attendees, an empty array if there are none. The # attendees are objects of Icalendar::Address. If +uri+ is specified # only the return the attendees with this +uri+. def attendees(uri = nil) attendees = @properties.enum_by_name('ATTENDEE').map { |a| Icalendar::Address.decode(a) } attendees.freeze if uri attendees.select { |a| a == uri } else attendees end end # Return true if the +uri+, usually a mailto: URI, is an attendee. def attendee?(uri) attendees.include? uri end # This property defines the categories for a calendar component. # # Property Name: CATEGORIES # # Value Type: TEXT # # Ruby Type: Array of String # # This property is used to specify categories or subtypes of the # calendar component. The categories are useful in searching for a # calendar component of a particular type and category. def categories proptextlistarray 'CATEGORIES' end def comments proptextarray 'COMMENT' end def contacts proptextarray 'CONTACT' end # An Array of attachments, see Attachment for more information. def attachments @properties.enum_by_name('ATTACH').map do |f| attachment = Attachment.decode(f, 'uri', 'FMTTYPE') end end end end module Set # Properties common to Vevent, Vtodo, and Vjournal. module Common # Set the access class of the component, see Icalendar::Property::Common#access_class. def access_class(token) set_token 'CLASS', ["PUBLIC", "PRIVATE", "CONFIDENTIAL"], "PUBLIC", token end # Set the creation time, see Icalendar::Property::Common#created def created(time) set_datetime 'CREATED', time end # Set the description, see Icalendar::Property::Common#description. def description(text) set_text 'DESCRIPTION', text end # Set the sequence number, see Icalendar::Property::Common#sequence. # is no SEQUENCE; property. def sequence(int) set_integer 'SEQUENCE', int end # Set the timestamp, see Icalendar::Property::Common#timestamp. def dtstamp(time) set_datetime 'DTSTAMP', time self end # The start time or date, see Icalendar::Property::Common#dtstart. def dtstart(start) set_date_or_datetime 'DTSTART', 'DATE-TIME', start self end # Set the last modification time, see Icalendar::Property::Common#lastmod. def lastmod(time) set_datetime 'LAST-MODIFIED', time self end # Set the event organizer, an Icalendar::Address, see Icalendar::Property::Common#organizer. # # Without an +adr+ it yields an Icalendar::Address that is a copy of # the current organizer (if any), allowing it to be modified. def organizer(adr=nil) #:yield: organizer unless adr adr = @comp.organizer if adr adr = adr.copy else adr = Icalendar::Address.create end yield adr end set_address('ORGANIZER', adr) self end =begin # Status values are not rejected during decoding. However, if the # status is requested, and it's value is not one of the defined # allowable values, an exception is raised. def status case self when Vpim::Icalendar::Vevent proptoken 'STATUS', ['TENTATIVE', 'CONFIRMED', 'CANCELLED'] when Vpim::Icalendar::Vtodo proptoken 'STATUS', ['NEEDS-ACTION', 'COMPLETED', 'IN-PROCESS', 'CANCELLED'] when Vpim::Icalendar::Vevent proptoken 'STATUS', ['DRAFT', 'FINAL', 'CANCELLED'] end end =end # Set summary description of component, see Icalendar::Property::Common#summary. def summary(text) set_text 'SUMMARY', text end # Set the unique identifier of this calendar component, see Icalendar::Property::Common#uid. def uid(uid) set_text 'UID', uid end def url(url) set_text 'URL', url end # Add an attendee Address, see Icalendar::Property::Common#attendees. def add_attendee(adr) add_address('ATTENDEE', adr) end # Set the categories, see Icalendar::Property::Common#attendees. # # If +cats+ is provided, the categories are set to cats, either a # String or an Array of String. Otherwise, and array of the existing # category strings is yielded, and it can be modified. def categories(cats = nil) #:yield: categories unless cats cats = @comp.categories yield cats end # TODO - strip the strings set_text_list('CATEGORIES', cats) end # Set the comment, see Icalendar::Property::Common#comments. def comment(value) set_text 'COMMENT', value end =begin def contacts proptextarray 'CONTACT' end # An Array of attachments, see Attachment for more information. def attachments @properties.enum_by_name('ATTACH').map do |f| attachment = Attachment.decode(f, 'uri', 'FMTTYPE') end end =end end end end end vpim-0.695/lib/vpim/property/location.rb0000644000076500000240000000167711152674120016727 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end module Vpim class Icalendar module Property module Location # Physical location information relevant to the component, or nil if # there is no LOCATION property. def location proptext 'LOCATION' end # Array of Float, +[ latitude, longitude]+. # # North of the equator is positive latitude, east of the meridian is # positive longitude. # # See RFC2445 for more info... there are lots of special cases. def geo prop = @properties.detect { |f| f.name? 'GEO' } if prop prop = Vpim.decode_list(prop.value_raw, ';') do |item| item.to_f end end prop end end end end end vpim-0.695/lib/vpim/property/priority.rb0000644000076500000240000000212611152674120016766 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end module Vpim class Icalendar module Property module Priority # +priority+ is a number from 1 to 9, with 1 being the highest # priority, 9 being the lowest. 0 means "no priority", equivalent to # not specifying the PRIORITY field. # # The other integer values are reserved by RFC2445. # # TODO # - methods to compare priorities? # - return as class Priority, with #to_i, and #to_s, and appropriate # comparison operators? def priority p = @properties.detect { |f| f.name? 'PRIORITY' } if !p p = 0 else p = p.value.to_i if( p < 0 || p > 9 ) raise Vpim::InvalidEncodingError, 'Invalid priority #{@priority} - it must be 0-9!' end end p end end end end end vpim-0.695/lib/vpim/property/recurrence.rb0000644000076500000240000000360111152674120017241 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require "enumerator" module Vpim class Icalendar module Property # Occurrences are calculated from DTSTART and RRULE. If there is no # RRULE, the component occurs only once, at the start time. # # Limitations: # # Only a single RRULE: is currently supported, this is the most common # case. module Recurrence def rrule #:nodoc: start = dtstart unless start raise ArgumentError, "Components without a DTSTART don't have occurrences!" end Vpim::Rrule.new(start, propvalue('RRULE')) end # The times this components occurs. If a block is not provided, returns # an enumerator. # # Occurrences may be infinite, +dountil+ can be provided to limit the # iterations, see Rrule#each. def occurrences(dountil = nil, &block) #:yield: occurrence time rr = rrule unless block_given? return Enumerable::Enumerator.new(self, :occurrences, dountil) end rr.each(dountil, &block) end alias occurences occurrences #:nodoc: backwards compatibility # True if this components occurs in a time period later than +t0+, but # earlier than +t1+. def occurs_in?(t0, t1) # TODO - deprecate this, its a hack occurrences(t1).detect do |tend| if respond_to? :duration tend += duration || 0 end tend >= t0 end end def rdates #:nodoc: # TODO - this is a hack, remove it Vpim.decode_date_time_list(propvalue('RDATE')) end end end end end vpim-0.695/lib/vpim/property/resources.rb0000644000076500000240000000060511152674120017117 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end module Vpim class Icalendar module Property module Resources def resources proptextlistarray 'RESOURCES' end end end end end vpim-0.695/lib/vpim/repo.rb0000644000076500000240000001375111152674120014174 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require 'enumerator' require "net/http" require 'plist' require 'vpim/icalendar' require 'vpim/duration' module Vpim # A Repo is a representation of a calendar repository. # # Currently supported repository types are: # - Repo::Apple3, an Apple iCal3 repository. # - Repo::Directory, a directory hierarchy containing .ics files # - Repo::Uri, a URI that identifies a single iCalendar # # All repository types support at least the methods of Repo, and all # repositories return calendars that support at least the methods of # Repo::Calendar. class Repo include Enumerable # Open a repository at location +where+. def initialize(where) end # Enumerate the calendars in the repository. def each #:yield: calendar end # A calendar abstraction. It models a calendar in a calendar repository # that may not be an iCalendar. # # It has methods that behave identically to Icalendar, but it also has # methods like name and displayed that are not present in an iCalendar. class Calendar include Enumerable # The calendar name. def name end # Whether a calendar should be displayed. # # TODO - should be #displayed? def displayed end # Encode into iCalendar format. def encode end # Enumerate the components in the calendar, both todos and events, or # the specified klass. Like Icalendar#each() def each(klass=nil, &block) #:yield: component end # Enumerate the events in the calendar. def events(&block) #:yield: Vevent each(Vpim::Icalendar::Vevent, &block) end # Enumerate the todos in the calendar. def todos(&block) #:yield: Vtodo each(Vpim::Icalendar::Vtodo, &block) end # The method definitions are just to fool rdoc, not to be used. %w{each name displayed encode}.each{|m| remove_method m} def file_each(file, klass, &block) #:nodoc: unless iterator? return Enumerable::Enumerator.new(self, :each, klass) end cals = open(file) do |io| Vpim::Icalendar.decode(io) end cals.each do |cal| cal.each(klass, &block) end self end end end class Repo include Enumerable # An Apple iCal version 3 repository. class Apple3 < Repo def initialize(where = "~/Library/Calendars") @where = where.to_str end def each #:nodoc: Dir[ File.expand_path(@where + "/**/*.calendar") ].each do |dir| yield Calendar.new(dir) end self end class Calendar < Repo::Calendar def initialize(dir) # :nodoc: @dir = dir end def plist(key) #:nodoc: Plist::parse_xml( @dir + "/Info.plist")[key] end def name #:nodoc: plist "Title" end def displayed #:nodoc: 1 == plist("Checked") end def each(klass=nil, &block) #:nodoc: unless iterator? return Enumerable::Enumerator.new(self, :each, klass) end Dir[ @dir + "/Events/*.ics" ].map do |ics| file_each(ics, klass, &block) end self end def encode #:nodoc: Icalendar.create2 do |cal| each{|c| cal << c} end.encode end end end class Directory < Repo class Calendar < Repo::Calendar def initialize(file) #:nodoc: @file = file end def name #:nodoc: File.basename(@file) end def displayed #:nodoc: true end def each(klass, &block) #:nodoc: file_each(@file, klass, &block) end def encode #:nodoc: open(@file, "r"){|f| f.read} end end def initialize(where = ".") @where = where.to_str end def each #:nodoc: Dir[ File.expand_path(@where + "/**/*.ics") ].each do |file| yield Calendar.new(file) end self end end class Uri < Repo def self.uri_check(uri) uri = case uri when URI uri else begin URI.parse(uri) rescue URI::InvalidURIError => e raise ArgumentError, "Invalid URI for #{uri.inspect} - #{e.to_s}" end end unless uri.scheme == "http" raise ArgumentError, "Unsupported URI scheme for #{uri.inspect}" end uri end class Calendar < Repo::Calendar def body end def initialize(uri) #:nodoc: @uri = Uri.uri_check(uri) end def name #:nodoc: @uri.to_s end def displayed #:nodoc: true end def each(klass, &block) #:nodoc: unless iterator? return Enumerable::Enumerator.new(self, :each, klass) end cals = Vpim::Icalendar.decode(encode) cals.each do |cal| cal.each(klass, &block) end self end def encode #:nodoc: Net::HTTP.get_response(@uri) do |result| accum = "" =begin better to let this pass up as an invalid encoding error if result.code != "200" raise StandardError, "HTTP GET of #{@uri.to_s.inspect} failed with #{result.code} #{result.error_type}" end =end result.read_body do |chunk| accum << chunk end return accum end end end def initialize(where) @where = Uri.uri_check(where) end def each #:nodoc: yield Calendar.new(@where) self end end end end vpim-0.695/lib/vpim/rfc2425.rb0000644000076500000240000002517411152674120014320 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require 'vpim/vpim' module Vpim # Contains regular expression strings for the EBNF of RFC 2425. module Bnf #:nodoc: # 1*(ALPHA / DIGIT / "-") # Note: I think I can add A-Z here, and get rid of the "i" matches elsewhere. # Note: added '_' to allowed because its produced by Notes (X-LOTUS-CHILD_UID:) # Note: added '/' to allowed because its produced by KAddressBook (X-messaging/xmpp-All:) # Note: added ' ' to allowed because its produced by highrisehq.com (X-GOOGLE TALK:) NAME = '[-a-z0-9_/][-a-z0-9_/ ]*' # <"> <"> QSTR = '"([^"]*)"' # * PTEXT = '([^";:,]+)' # param-value = ptext / quoted-string PVALUE = "(?:#{QSTR}|#{PTEXT})" # param = name "=" param-value *("," param-value) # Note: v2.1 allows a type or encoding param-value to appear without the type= # or the encoding=. This is hideous, but we try and support it, if there # is no "=", then $2 will be "", and we will treat it as a v2.1 param. PARAM = ";(#{NAME})(=?)((?:#{PVALUE})?(?:,#{PVALUE})*)" # V3.0: contentline = [group "."] name *(";" param) ":" value # V2.1: contentline = *( group "." ) name *(";" param) ":" value # # We accept the V2.1 syntax for backwards compatibility. #LINE = "((?:#{NAME}\\.)*)?(#{NAME})([^:]*)\:(.*)" LINE = "^((?:#{NAME}\\.)*)?(#{NAME})((?:#{PARAM})*):(.*)$" # date = date-fullyear ["-"] date-month ["-"] date-mday # date-fullyear = 4 DIGIT # date-month = 2 DIGIT # date-mday = 2 DIGIT DATE = '(\d\d\d\d)-?(\d\d)-?(\d\d)' # time = time-hour [":"] time-minute [":"] time-second [time-secfrac] [time-zone] # time-hour = 2 DIGIT # time-minute = 2 DIGIT # time-second = 2 DIGIT # time-secfrac = "," 1*DIGIT # time-zone = "Z" / time-numzone # time-numzone = sign time-hour [":"] time-minute TIME = '(\d\d):?(\d\d):?(\d\d)(\.\d+)?(Z|[-+]\d\d:?\d\d)?' # integer = (["+"] / "-") 1*DIGIT INTEGER = '[-+]?\d+' # QSAFE-CHAR = WSP / %x21 / %x23-7E / NON-US-ASCII # ; Any character except CTLs and DQUOTE QSAFECHAR = '[ \t\x21\x23-\x7e\x80-\xff]' # SAFE-CHAR = WSP / %x21 / %x23-2B / %x2D-39 / %x3C-7E / NON-US-ASCII # ; Any character except CTLs, DQUOTE, ";", ":", "," SAFECHAR = '[ \t\x21\x23-\x2b\x2d-\x39\x3c-\x7e\x80-\xff]' end end module Vpim # Split on \r\n or \n to get the lines, unfold continued lines (they # start with ' ' or \t), and return the array of unfolded lines. # # This also supports the (invalid) encoding convention of allowing empty # lines to be inserted for readability - it does this by dropping zero-length # lines. def Vpim.unfold(card) #:nodoc: unfolded = [] card.each do |line| line.chomp! # If it's a continuation line, add it to the last. # If it's an empty line, drop it from the input. if( line =~ /^[ \t]/ ) unfolded[-1] << line[1, line.size-1] elsif( line =~ /^$/ ) else unfolded << line end end unfolded end # Convert a +sep+-seperated list of values into an array of values. def Vpim.decode_list(value, sep = ',') # :nodoc: list = [] value.each(sep) do |item| item.chomp!(sep) list << yield(item) end list end # Convert a RFC 2425 date into an array of [year, month, day]. def Vpim.decode_date(v) # :nodoc: unless v =~ %r{^\s*#{Bnf::DATE}\s*$} raise Vpim::InvalidEncodingError, "date not valid (#{v})" end [$1.to_i, $2.to_i, $3.to_i] end # Convert a RFC 2425 date into a Date object. def self.decode_date_to_date(v) Date.new(*decode_date(v)) end # Note in the following the RFC2425 allows yyyy-mm-ddThh:mm:ss, but RFC2445 # does not. I choose to encode to the subset that is valid for both. # Encode a Date object as "yyyymmdd". def Vpim.encode_date(d) # :nodoc: "%0.4d%0.2d%0.2d" % [ d.year, d.mon, d.day ] end # Encode a Date object as "yyyymmdd". def Vpim.encode_time(d) # :nodoc: "%0.4d%0.2d%0.2d" % [ d.year, d.mon, d.day ] end # Encode a Time or DateTime object as "yyyymmddThhmmss" def Vpim.encode_date_time(d) # :nodoc: "%0.4d%0.2d%0.2dT%0.2d%0.2d%0.2d" % [ d.year, d.mon, d.day, d.hour, d.min, d.sec ] end # Convert a RFC 2425 time into an array of [hour,min,sec,secfrac,timezone] def Vpim.decode_time(v) # :nodoc: unless match = %r{^\s*#{Bnf::TIME}\s*$}.match(v) raise Vpim::InvalidEncodingError, "time '#{v}' not valid" end hour, min, sec, secfrac, tz = match.to_a[1..5] [hour.to_i, min.to_i, sec.to_i, secfrac ? secfrac.to_f : 0, tz] end def self.array_datetime_to_time(dtarray) #:nodoc: # We get [ year, month, day, hour, min, sec, usec, tz ] begin tz = (dtarray.pop == "Z") ? :gm : :local Time.send(tz, *dtarray) rescue ArgumentError => e raise Vpim::InvalidEncodingError, "#{tz} #{e} (#{dtarray.join(', ')})" end end # Convert a RFC 2425 time into an array of Time objects. def Vpim.decode_time_to_time(v) # :nodoc: array_datetime_to_time(decode_date_time(v)) end # Convert a RFC 2425 date-time into an array of [year,mon,day,hour,min,sec,secfrac,timezone] def Vpim.decode_date_time(v) # :nodoc: unless match = %r{^\s*#{Bnf::DATE}T#{Bnf::TIME}\s*$}.match(v) raise Vpim::InvalidEncodingError, "date-time '#{v}' not valid" end year, month, day, hour, min, sec, secfrac, tz = match.to_a[1..8] [ # date year.to_i, month.to_i, day.to_i, # time hour.to_i, min.to_i, sec.to_i, secfrac ? secfrac.to_f : 0, tz ] end def Vpim.decode_date_time_to_datetime(v) #:nodoc: year, month, day, hour, min, sec, secfrac, tz = Vpim.decode_date_time(v) # TODO - DateTime understands timezones, so we could decode tz and use it. DateTime.civil(year, month, day, hour, min, sec, 0) end # Vpim.decode_boolean # # float # # float_list =begin =end # Convert an RFC2425 INTEGER value into an Integer def Vpim.decode_integer(v) # :nodoc: unless match = %r{\s*#{Bnf::INTEGER}\s*}.match(v) raise Vpim::InvalidEncodingError, "integer not valid (#{v})" end v.to_i end # # integer_list # Convert a RFC2425 date-list into an array of dates. def Vpim.decode_date_list(v) # :nodoc: Vpim.decode_list(v) do |date| date.strip! if date.length > 0 Vpim.decode_date(date) end end.compact end # Convert a RFC 2425 time-list into an array of times. def Vpim.decode_time_list(v) # :nodoc: Vpim.decode_list(v) do |time| time.strip! if time.length > 0 Vpim.decode_time(time) end end.compact end # Convert a RFC 2425 date-time-list into an array of date-times. def Vpim.decode_date_time_list(v) # :nodoc: Vpim.decode_list(v) do |datetime| datetime.strip! if datetime.length > 0 Vpim.decode_date_time(datetime) end end.compact end # Convert RFC 2425 text into a String. # \\ -> \ # \n -> NL # \N -> NL # \, -> , # \; -> ; # # I've seen double-quote escaped by iCal.app. Hmm. Ok, if you aren't supposed # to escape anything but the above, everything else is ambiguous, so I'll # just support it. def Vpim.decode_text(v) # :nodoc: # FIXME - I think this should trim leading and trailing space v.gsub(/\\(.)/) do case $1 when 'n', 'N' "\n" else $1 end end end def Vpim.encode_text(v) #:nodoc: v.to_str.gsub(/([\\,;\n])/) { $1 == "\n" ? "\\n" : "\\"+$1 } end # v is an Array of String, or just a single String def Vpim.encode_text_list(v, sep = ",") #:nodoc: begin v.to_ary.map{ |t| Vpim.encode_text(t) }.join(sep) rescue Vpim.encode_text(v) end end # Convert a +sep+-seperated list of TEXT values into an array of values. def Vpim.decode_text_list(value, sep = ',') # :nodoc: # Need to do in two stages, as best I can find. list = value.scan(/([^#{sep}\\]*(?:\\.[^#{sep}\\]*)*)#{sep}/).map do |v| Vpim.decode_text(v.first) end if value.match(/([^#{sep}\\]*(?:\\.[^#{sep}\\]*)*)$/) list << $1 end list end # param-value = paramtext / quoted-string # paramtext = *SAFE-CHAR # quoted-string = DQUOTE *QSAFE-CHAR DQUOTE def Vpim.encode_paramtext(value) case value when %r{\A#{Bnf::SAFECHAR}*\z} value else raise Vpim::Unencodable, "paramtext #{value.inspect}" end end def Vpim.encode_paramvalue(value) case value when %r{\A#{Bnf::SAFECHAR}*\z} value when %r{\A#{Bnf::QSAFECHAR}*\z} '"' + value + '"' else raise Vpim::Unencodable, "param-value #{value.inspect}" end end # Unfold the lines in +card+, then return an array of one Field object per # line. def Vpim.decode(card) #:nodoc: content = Vpim.unfold(card).collect { |line| DirectoryInfo::Field.decode(line) } end # Expand an array of fields into its syntactic entities. Each entity is a sequence # of fields where the sequences is delimited by a BEGIN/END field. Since # BEGIN/END delimited entities can be nested, we build a tree. Each entry in # the array is either a Field or an array of entries (where each entry is # either a Field, or an array of entries...). def Vpim.expand(src) #:nodoc: # output array to expand the src to dst = [] # stack used to track our nesting level, as we see begin/end we start a # new/finish the current entity, and push/pop that entity from the stack current = [ dst ] for f in src if f.name? 'BEGIN' e = [ f ] current.last.push(e) current.push(e) elsif f.name? 'END' current.last.push(f) unless current.last.first.value? current.last.last.value raise "BEGIN/END mismatch (#{current.last.first.value} != #{current.last.last.value})" end current.pop else current.last.push(f) end end dst end # Split an array into an array of all the fields at the outer level, and # an array of all the inner arrays of fields. Return the array [outer, # inner]. def Vpim.outer_inner(fields) #:nodoc: # TODO - use Enumerable#partition # seperate into the outer-level fields, and the arrays of component # fields outer = [] inner = [] fields.each do |line| case line when Array; inner << line else; outer << line end end return outer, inner end end vpim-0.695/lib/vpim/rrule.rb0000755000076500000240000003744211152674120014366 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require 'vpim/rfc2425' require 'vpim/date' require 'vpim/time' require 'vpim/vpim' =begin require 'pp' $debug = ENV['DEBUG'] class Date def inspect self.to_s end end def debug(*objs) if $debug pp(*objs) print ' (', caller(1)[0], ')', "\n" end end =end module Vpim # Implements the iCalendar recurrence rule syntax. See etc/rrule.txt for the # syntax description and examples from RFC 2445. The description is pretty # hard to understand, but the examples are more helpful. # # The implementation is reasonably complete, but still lacks support for: # # Recurrence by date (RDATE) and exclusions (EXDATE, EXRULE). # # TODO - BYWEEKNO: rules that are limited to particular weeks in a year. # # TODO - BYHOUR, BYMINUTE, BYSECOND: trivial to do, but I don't have an # immediate need for them. # # TODO - new API? -> Rrule#infinite? # # == Examples # # - link:rrule.txt: utility for printing recurrence rules class Rrule include Enumerable # The recurrence rule, +rrule+, specifies how to generate a set of times # from a start time, +dtstart+ (which must the first of the set of # recurring times). If +rrule+ is nil, the set contains only +dtstart+. def initialize(dtstart, rrule = nil) @dtstart = dtstart.getlocal # The getlocal is a hack so that UTC times get converted to local, # because yielded times are always local, because we don't support # timezones. @rrule = rrule # Freq is mandatory, but must occur only once. @freq = nil # Both Until and Count must not occur, neither is OK. @until = nil @count = nil # Interval is optional, but defaults to 1. @interval = 1 # WKST defines what day a week begins on, the default is monday. @wkst = 'MO' # Recurrence can modified by these. @by = {} if @rrule @rrule.scan(/([^;=]+)=([^;=]+)/) do |key,value| key.upcase! value.upcase! case key when 'FREQ' @freq = value when 'UNTIL' if @count raise "found UNTIL, but COUNT already specified" end @until = Rrule.time_from_rfc2425(value) when 'COUNT' if @until raise "found COUNT, but UNTIL already specified" end @count = value.to_i when 'INTERVAL' @interval = value.to_i if @interval < 1 raise "interval must be a positive integer" end when 'WKST' # TODO - check value is MO-SU @wkst = value else @by[key] = value end end if !@freq # TODO - this shouldn't be an arg error, but a FormatError, its not the # caller's fault! raise ArgumentError, "recurrence rule lacks a frequency" end end end # Return an Enumerable, it's #each() will yield over all occurrences up to # (and not including) time +dountil+. def each_until(dountil) Vpim::Enumerator.new(self, dountil) end # Yields for each +ytime+ in the recurring set of events. # # Warning: the set may be infinite! If you need an upper bound on the # number of occurrences, you need to implement a count, or pass a time, # +dountil+, which will not be iterated past (i.e. all times yielded will be # less than +dountil+). # # Also, iteration will not currently continue past the limit of a Time # object, which is some time in 2037 with the 32-bit time_t common on # most systems. def each(dountil = nil) #:yield: ytime t = @dtstart.clone # Time.to_a => [ sec, min, hour, day, month, year, wday, yday, isdst, zone ] # Every event occurs at its start time, but only if the start time is # earlier than DOUNTIL... if !dountil || t < dountil yield t end count = 1 # With no recurrence, DTSTART is the only occurrence. if !@rrule return self end loop do # Build the set of times to yield within this interval (and after # DTSTART) days = DaySet.new(t) hour = nil min = nil sec = nil # Need to make a Dates class, and make month an instance of it, and add # the "intersect" operator. case @freq #when 'YEARLY' then # Don't need to keep track of year, all occurrences are within t's # year. when 'MONTHLY' then days.month = t.month when 'WEEKLY' then #days.month = t.month # TODO - WEEKLY when 'DAILY' then days.mday = t.month, t.mday when 'HOURLY' then hour = [t.hour] when 'MINUTELY' then min = [t.min] when 'SECONDLY' then sec = [t.sec] end # debug [t, days] # Process the BY* modifiers in RFC defined order: # BYMONTH, # BYWEEKNO, # BYYEARDAY, # BYMONTHDAY, # BYDAY, # BYHOUR, # BYMINUTE, # BYSECOND, # BYSETPOS bymon = [nil] if @by['BYMONTH'] bymon = @by['BYMONTH'].split(',') bymon = bymon.map { |m| m.to_i } # debug bymon # In yearly, at this point, month will always be nil. At other # frequencies, it will not. days.intersect_bymon(bymon) # debug days end # TODO - BYWEEKNO if @by['BYYEARDAY'] byyday = @by['BYYEARDAY'].scan(/,?([+-]?[1-9]\d*)/) # debug byyday dates = byyearday(t.year, byyday) days.intersect_dates(dates) end if @by['BYMONTHDAY'] bymday = @by['BYMONTHDAY'].scan(/,?([+-]?[1-9]\d*)/) # debug bymday # Generate all days matching this for all months. For yearly, this # is what we want, for anything of monthly or higher frequency, it # is too many days, but that's OK, since the month will already # be specified and intersection will eliminate the out-of-range # dates. dates = bymonthday(t.year, bymday) # debug dates days.intersect_dates(dates) # debug days end if @by['BYDAY'] byday = @by['BYDAY'].scan(/,?([+-]?[1-9]?\d*)?(SU|MO|TU|WE|TH|FR|SA)/i) # BYDAY means different things in different frequencies. The +n+ # is only meaningful when freq is yearly or monthly. case @freq when 'YEARLY' dates = bymon.map { |m| byday_in_monthly(t.year, m, byday) }.flatten when 'MONTHLY' dates = byday_in_monthly(t.year, t.month, byday) when 'WEEKLY' dates = byday_in_weekly(t.year, t.month, t.mday, @wkst, byday) when 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY' # Reuse the byday_in_monthly. Current day is already specified, # so this will just eliminate the current day if its not allowed # in BYDAY. dates = byday_in_monthly(t.year, t.month, byday) end # debug dates days.intersect_dates(dates) # debug days end # TODO - BYHOUR, BYMINUTE, BYSECOND hour = [@dtstart.hour] if !hour min = [@dtstart.min] if !min sec = [@dtstart.sec] if !sec # debug days # Generate the yield set so BYSETPOS can be evaluated. yset = [] days.each do |m,d| hour.each do |h| min.each do |n| sec.each do |s| y = Time.local(t.year, m, d, h, n, s, 0) next if y.hour != h yset << y end end end end if @by['BYSETPOS'] bysetpos = @by['BYSETPOS'].split(',') yset = bysetpos.map do |i| i = i.to_i case when i < 0 # yset[-1] is last yset[i] when i > 0 # yset[1] is first yset[i-1] else # ignore invalid syntax end end.compact # set positions out of scope will be nil, RFC says ignore them end # Yield the occurrence, if we haven't gone over COUNT, or past UNTIL, or # past the end of representable time. yset.each do |y| # The generated set can sometimes generate results earlier # than the DTSTART, skip them. Also, we already yielded # DTSTART, skip it. next if y <= @dtstart count += 1 # We are done if current count is past @count. if(@count && (count > @count)) return self end # We are done if current time is past @until. if @until && (y > @until) return self end # We are also done if current time is past the # caller-requested until. if dountil && (y >= dountil) return self end yield y end # Add @interval to @freq component # Note - when we got past representable time, the error is: # time out of range (ArgumentError) # Finish when we see this. begin case @freq when 'YEARLY' then t = t.plus_year(@interval) when 'MONTHLY' then t = t.plus_month(@interval) when 'WEEKLY' then t = t.plus_day(@interval * 7) when 'DAILY' then t = t.plus_day(@interval) when 'HOURLY' then t += @interval * 60 * 60 when 'MINUTELY' then t += @interval * 60 when 'SECONDLY' then t += @interval when nil return self end rescue ArgumentError return self if $!.message =~ /^time out of range$/ raise ArgumentError, "#{$!.message} while adding interval to #{t.inspect}" end return self if dountil && (t > dountil) end end class DaySet #:nodoc: def initialize(ref) @ref = ref # Need to know because leap years have an extra day, and to get # our defaults. @month = nil @week = nil end def month=(mon) @month = { mon => nil } end def week=(week) @week = week end def mday=(pair) @month = { pair[0] => [ pair[1] ] } end def intersect_bymon(bymon) #:nodoc: if !@month @month = {} bymon.each do |m| @month[m] = nil end else @month.delete_if { |m, days| ! bymon.include? m } end end def intersect_dates(dates) #:nodoc: return unless dates # If no months are in the dayset, add all the ones in dates if !@month @month = {} dates.each do |d| @month[d.mon] = nil end end # In each month, # if there are days, # eliminate those not in dates # otherwise # add all those in dates @month.each do |mon, days| days_in_mon = dates.find_all { |d| d.mon == mon } days_in_mon = days_in_mon.map { |d| d.day } if days days_in_mon = days_in_mon & days end @month[mon] = days_in_mon end end def each @month = { @ref.month => [ @ref.mday ] } if !@month @month.each_key do |m| @month[m] = [@ref.day] if !@month[m] # FIXME - if @ref.day is 31, and the month doesn't have 32 days, we'll # generate invalid dates here, check for that, and eliminate them end @month.keys.sort.each do |m| @month[m].sort.each do |d| yield m, d end end end end def self.time_from_rfc2425(str) #:nodoc: # With ruby1.8 we can use DateTime to do this quick-n-easy: # dt = DateTime.parse(str) # Time.local(dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec, 0) # The time can be a DATE or a DATE-TIME, the latter always has a 'T' in it. if str =~ /T/ d = Vpim.decode_date_time(str) # We get [ year, month, day, hour, min, sec, usec, tz ] if(d.pop == "Z") t = Time.gm(*d) else t = Time.local(*d) end else d = Vpim.decode_date(str) # We get [ year, month, day ] # FIXME - I have to choose gm or local, though neither makes much # sense. This is a bit of a hack - what we should really do is return # an instance of Date, and Time should allow itself to be compared to # Date... This hack will give odd results when comparing times, because # it will create a Time on the right date but whos time is 00:00:00. t = Time.local(*d) end if t.month != d[1] || t.day != d[2] || (d[3] && t.hour != d[3]) raise Vpim::InvalidEncodingError, "Error - datetime does not exist" end t end def bymonthday(year, bymday) #:nodoc: dates = [] bymday.each do |mday| dates |= DateGen.bymonthday(year, nil, mday[0].to_i) end dates.sort! dates end def byyearday(year, byyday) #:nodoc: dates = [] byyday.each do |yday| dates << Date.ordinal(year, yday[0].to_i) end dates.sort! dates end def byday_in_monthly(year, mon, byday) #:nodoc: dates = [] byday.each do |rule| if rule[0].empty? n = nil else n = rule[0].to_i end dates |= DateGen.bywday(year, mon, Date.str2wday(rule[1]), n) end dates.sort! dates end def byday_in_weekly(year, mon, day, wkst, byday) #:nodoc: # debug ["day", year,mon,day,wkst,byday] days = byday.map{ |_, byday| Date.str2wday(byday) } week = DateGen.weekofdate(year, mon, day, wkst) # debug [ "week", dates ] week.delete_if do |d| !days.include?(d.wday) end week end # Help encode an RRULE value. # # TODO - the Maker is both incomplete, and its a bit cheesy, I'd like to do # something that is a kind of programmatic version of the UI that iCal has. class Maker def initialize(&block) #:yield: self @freq = nil @until = nil @count = nil @interval = nil @wkst = nil @by = {} if block yield self end end FREQ = %w{ YEARLY WEEKLY MONTHLY DAILY } #:nodoc: incomplete! def frequency=(freq) freq = freq.to_str.upcase unless FREQ.include? freq raise ArgumentError, "Frequency #{freq} is not valid" end @freq = freq end # +runtil+ is Time, Date, or DateTime def until=(runtil) if @count raise ArgumentError, "Cannot specify UNTIL if COUNT was specified" end @until = runtil end # +count+ is integral def count=(rcount) if @until raise ArgumentError, "Cannot specify COUNT if UNTIL was specified" end @count = rcount.to_int end # TODO - BY.... def encode unless @freq raise ArgumentError, "Must specify FREQUENCY" end rrule = "FREQ=#{@freq}" [ ["COUNT", @count], ["UNTIL", @until], # TODO... ].each do |k,v| if v rrule += ";#{k}=#{v}" end end rrule end end end end vpim-0.695/lib/vpim/time.rb0000644000076500000240000000233611152674120014162 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require 'date' # Extensions to builtin Time allowing addition to Time by multiples of other # intervals than a second. class Time # Returns a new Time, +years+ later than this time. Feb 29 of a # leap year will be rounded up to Mar 1 if the target date is not a leap # year. def plus_year(years) Time.local(year + years, month, day, hour, min, sec, usec) end # Returns a new Time, +months+ later than this time. The day will be # rounded down if it is not valid for that month. # Jan 31 plus 1 month will be on Feb 28! def plus_month(months) d = Date.new(year, month, day) d >>= months Time.local(d.year, d.month, d.day, hour, min, sec, usec) end # Returns a new Time, +days+ later than this time. # Does this do as I expect over DST? What if the hour doesn't exist # in the next day, due to DST changes? def plus_day(days) d = Date.new(year, month, day) d += days Time.local(d.year, d.month, d.day, hour, min, sec, usec) end end vpim-0.695/lib/vpim/vcard.rb0000644000076500000240000013202711152674120014324 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require 'vpim/vpim' require 'vpim/attachment' require 'vpim/dirinfo' require 'open-uri' require 'stringio' module Vpim # A vCard, a specialization of a directory info object. # # The vCard format is specified by: # - RFC2426: vCard MIME Directory Profile (vCard 3.0) # - RFC2425: A MIME Content-Type for Directory Information # # This implements vCard 3.0, but it is also capable of working with vCard 2.1 # if used with care. # # All line values can be accessed with Vcard#value, Vcard#values, or even by # iterating through Vcard#lines. Line types that don't have specific support # and non-standard line types ("X-MY-SPECIAL", for example) will be returned # as a String, with any base64 or quoted-printable encoding removed. # # Specific support exists to return more useful values for the standard vCard # types, where appropriate. # # The wrapper functions (#birthday, #nicknames, #emails, etc.) exist # partially as an API convenience, and partially as a place to document # the values returned for the more complex types, like PHOTO and EMAIL. # # For types that do not sensibly occur multiple times (like BDAY or GEO), # sometimes a wrapper exists only to return a single line, using #value. # However, if you find the need, you can still call #values to get all the # lines, and both the singular and plural forms will eventually be # implemented. # # If there is sufficient demand, specific support for vCard 2.1 could be # implemented. # # For more information see: # - link:rfc2426.txt: vCard MIME Directory Profile (vCard 3.0) # - link:rfc2425.txt: A MIME Content-Type for Directory Information # - http://www.imc.org/pdi/pdiproddev.html: vCard 2.1 Specifications # # vCards are usually transmitted in files with .vcf # extensions. # # = Examples # # - link:ex_mkvcard.txt: example of creating a vCard # - link:ex_cpvcard.txt: example of copying and them modifying a vCard # - link:ex_mkv21vcard.txt: example of creating version 2.1 vCard # - link:mutt-aliases-to-vcf.txt: convert a mutt aliases file to vCards # - link:ex_get_vcard_photo.txt: pull photo data from a vCard # - link:ab-query.txt: query the OS X Address Book to find vCards # - link:vcf-to-mutt.txt: query vCards for matches, output in formats useful # with Mutt (see link:README.mutt for details) # - link:tabbed-file-to-vcf.txt: convert a tab-delimited file to vCards, a # (small but) complete application contributed by Dane G. Avilla, thanks! # - link:vcf-to-ics.txt: example of how to create calendars of birthdays from vCards # - link:vcf-dump.txt: utility for dumping contents of .vcf files class Vcard < DirectoryInfo # Represents the value of an ADR field. # # #location, #preferred, and #delivery indicate information about how the # address is to be used, the other attributes are parts of the address. # # Using values other than those defined for #location or #delivery is # unlikely to be portable, or even conformant. # # All attributes are optional. #location and #delivery can be set to arrays # of strings. class Address # post office box (String) attr_accessor :pobox # seldom used, its not clear what it is for (String) attr_accessor :extended # street address (String) attr_accessor :street # usually the city (String) attr_accessor :locality # usually the province or state (String) attr_accessor :region # postal code (String) attr_accessor :postalcode # country name (String) attr_accessor :country # home, work (Array of String): the location referred to by the address attr_accessor :location # true, false (boolean): where this is the preferred address (for this location) attr_accessor :preferred # postal, parcel, dom (domestic), intl (international) (Array of String): delivery # type of this address attr_accessor :delivery # nonstandard types, their meaning is undefined (Array of String). These # might be found during decoding, but shouldn't be set during encoding. attr_reader :nonstandard # Used to simplify some long and tedious code. These symbols are in the # order required for the ADR field structured TEXT value, the order # cannot be changed. @@adr_parts = [ :@pobox, :@extended, :@street, :@locality, :@region, :@postalcode, :@country, ] # TODO # - #location? # - #delivery? def initialize #:nodoc: # TODO - Add #label to support LABEL. Try to find LABEL # in either same group, or with sam params. @@adr_parts.each do |part| instance_variable_set(part, '') end @location = [] @preferred = false @delivery = [] @nonstandard = [] end def encode #:nodoc: parts = @@adr_parts.map do |part| instance_variable_get(part) end value = Vpim.encode_text_list(parts, ";") params = [ @location, @delivery, @nonstandard ] params << 'pref' if @preferred params = params.flatten.compact.map { |s| s.to_str.downcase }.uniq paramshash = {} paramshash['TYPE'] = params if params.first Vpim::DirectoryInfo::Field.create( 'ADR', value, paramshash) end def Address.decode(card, field) #:nodoc: adr = new parts = Vpim.decode_text_list(field.value_raw, ';') @@adr_parts.each_with_index do |part,i| adr.instance_variable_set(part, parts[i] || '') end params = field.pvalues('TYPE') if params params.each do |p| p.downcase! case p when 'home', 'work' adr.location << p when 'postal', 'parcel', 'dom', 'intl' adr.delivery << p when 'pref' adr.preferred = true else adr.nonstandard << p end end # Strip duplicates [ adr.location, adr.delivery, adr.nonstandard ].each do |a| a.uniq! end end adr end end # Represents the value of an EMAIL field. class Email < String # true, false (boolean): whether this is the preferred email address attr_accessor :preferred # internet, x400 (String): the email address format, rarely specified # since the default is 'internet' attr_accessor :format # home, work (Array of String): the location referred to by the address. The # inclusion of location parameters in a vCard seems to be non-conformant, # strictly speaking, but also seems to be widespread. attr_accessor :location # nonstandard types, their meaning is undefined (Array of String). These # might be found during decoding, but shouldn't be set during encoding. attr_reader :nonstandard def initialize(email='') #:nodoc: @preferred = false @format = 'internet' @location = [] @nonstandard = [] super(email) end def inspect #:nodoc: s = "#<#{self.class.to_s}: #{to_str.inspect}" s << ", pref" if preferred s << ", #{format}" if format != 'internet' s << ", " << @location.join(", ") if @location.first s << ", #{@nonstandard.join(", ")}" if @nonstandard.first s end def encode #:nodoc: value = to_str.strip if value.length < 1 raise InvalidEncodingError, "EMAIL must have a value" end params = [ @location, @nonstandard ] params << @format if @format != 'internet' params << 'pref' if @preferred params = params.flatten.compact.map { |s| s.to_str.downcase }.uniq paramshash = {} paramshash['TYPE'] = params if params.first Vpim::DirectoryInfo::Field.create( 'EMAIL', value, paramshash) end def Email.decode(field) #:nodoc: value = field.to_text.strip if value.length < 1 raise InvalidEncodingError, "EMAIL must have a value" end eml = Email.new(value) params = field.pvalues('TYPE') if params params.each do |p| p.downcase! case p when 'home', 'work' eml.location << p when 'pref' eml.preferred = true when 'x400', 'internet' eml.format = p else eml.nonstandard << p end end # Strip duplicates [ eml.location, eml.nonstandard ].each do |a| a.uniq! end end eml end end # Represents the value of a TEL field. # # The value is supposed to be a "X.500 Telephone Number" according to RFC # 2426, but that standard is not freely available. Otherwise, anything that # looks like a phone number should be OK. class Telephone < String # true, false (boolean): whether this is the preferred email address attr_accessor :preferred # home, work, cell, car, pager (Array of String): the location # of the device attr_accessor :location # voice, fax, video, msg, bbs, modem, isdn, pcs (Array of String): the # capabilities of the device attr_accessor :capability # nonstandard types, their meaning is undefined (Array of String). These # might be found during decoding, but shouldn't be set during encoding. attr_reader :nonstandard def initialize(telephone='') #:nodoc: @preferred = false @location = [] @capability = [] @nonstandard = [] super(telephone) end def inspect #:nodoc: s = "#<#{self.class.to_s}: #{to_str.inspect}" s << ", pref" if preferred s << ", " << @location.join(", ") if @location.first s << ", " << @capability.join(", ") if @capability.first s << ", #{@nonstandard.join(", ")}" if @nonstandard.first s end def encode #:nodoc: value = to_str.strip if value.length < 1 raise InvalidEncodingError, "TEL must have a value" end params = [ @location, @capability, @nonstandard ] params << 'pref' if @preferred params = params.flatten.compact.map { |s| s.to_str.downcase }.uniq paramshash = {} paramshash['TYPE'] = params if params.first Vpim::DirectoryInfo::Field.create( 'TEL', value, paramshash) end def Telephone.decode(field) #:nodoc: value = field.to_text.strip if value.length < 1 raise InvalidEncodingError, "TEL must have a value" end tel = Telephone.new(value) params = field.pvalues('TYPE') if params params.each do |p| p.downcase! case p when 'home', 'work', 'cell', 'car', 'pager' tel.location << p when 'voice', 'fax', 'video', 'msg', 'bbs', 'modem', 'isdn', 'pcs' tel.capability << p when 'pref' tel.preferred = true else tel.nonstandard << p end end # Strip duplicates [ tel.location, tel.capability, tel.nonstandard ].each do |a| a.uniq! end end tel end end # The name from a vCard, including all the components of the N: and FN: # fields. class Name # family name, from N attr_accessor :family # given name, from N attr_accessor :given # additional names, from N attr_accessor :additional # such as "Ms." or "Dr.", from N attr_accessor :prefix # such as "BFA", from N attr_accessor :suffix # full name, the FN field. FN is a formatted version of the N field, # intended to be in a form more aligned with the cultural conventions of # the vCard owner than +formatted+ is. attr_accessor :fullname # all the components of N formtted as "#{prefix} #{given} #{additional} #{family}, #{suffix}" attr_reader :formatted # Override the attr reader to make it dynamic remove_method :formatted def formatted #:nodoc: f = [ @prefix, @given, @additional, @family ].map{|i| i == '' ? nil : i.strip}.compact.join(' ') if @suffix != '' f << ', ' << @suffix end f end def initialize(n='', fn='') #:nodoc: n = Vpim.decode_text_list(n, ';') do |item| item.strip end @family = n[0] || "" @given = n[1] || "" @additional = n[2] || "" @prefix = n[3] || "" @suffix = n[4] || "" # FIXME - make calls to #fullname fail if fn is nil @fullname = (fn || "").strip end def encode #:nodoc: Vpim::DirectoryInfo::Field.create('N', Vpim.encode_text_list([ @family, @given, @additional, @prefix, @suffix ].map{|n| n.strip}, ';') ) end def encode_fn #:nodoc: fn = @fullname.strip if @fullname.length == 0 fn = formatted end Vpim::DirectoryInfo::Field.create('FN', fn) end end def decode_invisible(field) #:nodoc: nil end def decode_default(field) #:nodoc: Line.new( field.group, field.name, field.value ) end def decode_version(field) #:nodoc: Line.new( field.group, field.name, (field.value.to_f * 10).to_i ) end def decode_text(field) #:nodoc: Line.new( field.group, field.name, Vpim.decode_text(field.value_raw) ) end def decode_n(field) #:nodoc: Line.new( field.group, field.name, Name.new(field.value, self['FN']).freeze ) end def decode_date_or_datetime(field) #:nodoc: date = nil begin date = Vpim.decode_date_to_date(field.value_raw) rescue Vpim::InvalidEncodingError date = Vpim.decode_date_time_to_datetime(field.value_raw) end Line.new( field.group, field.name, date ) end def decode_bday(field) #:nodoc: begin return decode_date_or_datetime(field) rescue Vpim::InvalidEncodingError # Hack around BDAY dates hat are correct in the month and day, but have # some kind of garbage in the year. if field.value =~ /^\s*(\d+)-(\d+)-(\d+)\s*$/ y = $1.to_i m = $2.to_i d = $3.to_i if(y < 1900) y = Time.now.year end Line.new( field.group, field.name, Date.new(y, m, d) ) else raise end end end def decode_geo(field) #:nodoc: geo = Vpim.decode_list(field.value_raw, ';') do |item| item.to_f end Line.new( field.group, field.name, geo ) end def decode_address(field) #:nodoc: Line.new( field.group, field.name, Address.decode(self, field) ) end def decode_email(field) #:nodoc: Line.new( field.group, field.name, Email.decode(field) ) end def decode_telephone(field) #:nodoc: Line.new( field.group, field.name, Telephone.decode(field) ) end def decode_list_of_text(field) #:nodoc: Line.new( field.group, field.name, Vpim.decode_text_list(field.value_raw).select{|t| t.length > 0}.uniq ) end def decode_structured_text(field) #:nodoc: Line.new( field.group, field.name, Vpim.decode_text_list(field.value_raw, ';') ) end def decode_uri(field) #:nodoc: Line.new( field.group, field.name, Attachment::Uri.new(field.value, nil) ) end def decode_agent(field) #:nodoc: case field.kind when 'text' decode_text(field) when 'uri' decode_uri(field) when 'vcard', nil Line.new( field.group, field.name, Vcard.decode(Vpim.decode_text(field.value_raw)).first ) else raise InvalidEncodingError, "AGENT type #{field.kind} is not allowed" end end def decode_attachment(field) #:nodoc: Line.new( field.group, field.name, Attachment.decode(field, 'binary', 'TYPE') ) end @@decode = { 'BEGIN' => :decode_invisible, # Don't return delimiter 'END' => :decode_invisible, # Don't return delimiter 'FN' => :decode_invisible, # Returned as part of N. 'ADR' => :decode_address, 'AGENT' => :decode_agent, 'BDAY' => :decode_bday, 'CATEGORIES' => :decode_list_of_text, 'EMAIL' => :decode_email, 'GEO' => :decode_geo, 'KEY' => :decode_attachment, 'LOGO' => :decode_attachment, 'MAILER' => :decode_text, 'N' => :decode_n, 'NAME' => :decode_text, 'NICKNAME' => :decode_list_of_text, 'NOTE' => :decode_text, 'ORG' => :decode_structured_text, 'PHOTO' => :decode_attachment, 'PRODID' => :decode_text, 'PROFILE' => :decode_text, 'REV' => :decode_date_or_datetime, 'ROLE' => :decode_text, 'SOUND' => :decode_attachment, 'SOURCE' => :decode_text, 'TEL' => :decode_telephone, 'TITLE' => :decode_text, 'UID' => :decode_text, 'URL' => :decode_uri, 'VERSION' => :decode_version, } @@decode.default = :decode_default # Cache of decoded lines/fields, so we don't have to decode a field more than once. attr_reader :cache #:nodoc: # An entry in a vCard. The #value object's type varies with the kind of # line (the #name), and on how the line was encoded. The objects returned # for a specific kind of line are often extended so that they support a # common set of methods. The goal is to allow all types of objects for a # kind of line to be treated with some uniformity, but still allow specific # handling for the various value types if desired. # # See the specific methods for details. class Line attr_reader :group attr_reader :name attr_reader :value def initialize(group, name, value) #:nodoc: @group, @name, @value = (group||''), name.to_str, value end def self.decode(decode, card, field) #:nodoc: card.cache[field] || (card.cache[field] = card.send(decode[field.name], field)) end end #@lines = {} FIXME - dead code # Return line for a field def f2l(field) #:nodoc: begin Line.decode(@@decode, self, field) rescue InvalidEncodingError # Skip invalidly encoded fields. end end # With no block, returns an Array of Line. If +name+ is specified, the # Array will only contain the +Line+s with that +name+. The Array may be # empty. # # If a block is given, each Line will be yielded instead of being returned # in an Array. def lines(name=nil) #:yield: Line # FIXME - this would be much easier if #lines was #each, and there was a # different #lines that returned an Enumerator that used #each unless block_given? map do |f| if( !name || f.name?(name) ) f2l(f) else nil end end.compact else each do |f| if( !name || f.name?(name) ) line = f2l(f) if line yield line end end end self end end private_class_method :new def initialize(fields, profile) #:nodoc: @cache = {} super(fields, profile) end # Create a vCard 3.0 object with the minimum required fields, plus any # +fields+ you want in the card (they can also be added later). def Vcard.create(fields = [] ) fields.unshift Field.create('VERSION', "3.0") super(fields, 'VCARD') end # Decode a collection of vCards into an array of Vcard objects. # # +card+ can be either a String or an IO object. # # Since vCards are self-delimited (by a BEGIN:vCard and an END:vCard), # multiple vCards can be concatenated into a single directory info object. # They may or may not be related. For example, AddressBook.app (the OS X # contact manager) will export multiple selected cards in this format. # # Input data will be converted from unicode if it is detected. The heuristic # is based on the first bytes in the string: # - 0xEF 0xBB 0xBF: UTF-8 with a BOM, the BOM is stripped # - 0xFE 0xFF: UTF-16 with a BOM (big-endian), the BOM is stripped and string # is converted to UTF-8 # - 0xFF 0xFE: UTF-16 with a BOM (little-endian), the BOM is stripped and string # is converted to UTF-8 # - 0x00 'B' or 0x00 'b': UTF-16 (big-endian), the string is converted to UTF-8 # - 'B' 0x00 or 'b' 0x00: UTF-16 (little-endian), the string is converted to UTF-8 # # If you know that you have only one vCard, then you can decode that # single vCard by doing something like: # # vcard = Vcard.decode(card_data).first # # Note: Should the import encoding be remembered, so that it can be reencoded in # the same format? def Vcard.decode(card) if card.respond_to? :to_str string = card.to_str elsif card.respond_to? :read string = card.read(nil) else raise ArgumentError, "Vcard.decode cannot be called with a #{card.type}" end case string when /^\xEF\xBB\xBF/ string = string.sub("\xEF\xBB\xBF", '') when /^\xFE\xFF/ arr = string.unpack('n*') arr.shift string = arr.pack('U*') when /^\xFF\xFE/ arr = string.unpack('v*') arr.shift string = arr.pack('U*') when /^\x00B/i string = string.unpack('n*').pack('U*') when /^B\x00/i string = string.unpack('v*').pack('U*') end entities = Vpim.expand(Vpim.decode(string)) # Since all vCards must have a begin/end, the top-level should consist # entirely of entities/arrays, even if its a single vCard. if entities.detect { |e| ! e.kind_of? Array } raise "Not a valid vCard" end vcards = [] for e in entities vcards.push(new(e.flatten, 'VCARD')) end vcards end # The value of the field named +name+, optionally limited to fields of # type +type+. If no match is found, nil is returned, if multiple matches # are found, the first match to have one of its type values be 'PREF' # (preferred) is returned, otherwise the first match is returned. # # FIXME - this will become an alias for #value. def [](name, type=nil) fields = enum_by_name(name).find_all { |f| type == nil || f.type?(type) } valued = fields.select { |f| f.value != '' } if valued.first fields = valued end # limit to preferred, if possible pref = fields.select { |f| f.pref? } if pref.first fields = pref end fields.first ? fields.first.value : nil end # Return the Line#value for a specific +name+, and optionally for a # specific +type+. # # If no line with the +name+ (and, optionally, +type+) exists, nil is # returned. # # If multiple lines exist, the order of preference is: # - lines with values over lines without # - lines with a type of 'pref' over lines without # If multiple lines are equally preferred, then the first line will be # returned. # # This is most useful when looking for a line that can not occur multiple # times, or when the line can occur multiple times, and you want to pick # the first preferred line of a specific type. See #values if you need to # access all the lines. # # Note that the +type+ field parameter is used for different purposes by # the various kinds of vCard lines, but for the addressing lines (ADR, # LABEL, TEL, EMAIL) it is has a reasonably consistent usage. Each # addressing line can occur multiple times, and a +type+ of 'pref' # indicates that a particular line is the preferred line. Other +type+ # values tend to indicate some information about the location ('home', # 'work', ...) or some detail about the address ('cell', 'fax', 'voice', # ...). See the methods for the specific types of line for information # about supported types and their meaning. def value(name, type = nil) v = nil fields = enum_by_name(name).find_all { |f| type == nil || f.type?(type) } valued = fields.select { |f| f.value != '' } if valued.first fields = valued end pref = fields.select { |f| f.pref? } if pref.first fields = pref end if fields.first line = begin Line.decode(@@decode, self, fields.first) rescue Vpim::InvalidEncodingError end if line return line.value end end nil end # A variant of #lines that only iterates over specific Line names. Since # the name is known, only the Line#value is returned or yielded. def values(name) unless block_given? lines(name).map { |line| line.value } else lines(name) { |line| yield line.value } end end # The first ADR value of type +type+, a Address. Any of the location or # delivery attributes of Address can be used as +type+. A wrapper around # #value('ADR', +type+). def address(type=nil) value('ADR', type) end # The ADR values, an array of Address. If a block is given, the values are # yielded. A wrapper around #values('ADR'). def addresses #:yield:address values('ADR') end # The AGENT values. Each AGENT value is either a String, a Uri, or a Vcard. # If a block is given, the values are yielded. A wrapper around # #values('AGENT'). def agents #:yield:agent values('AGENT') end # The BDAY value as either a Date or a DateTime, or nil if there is none. # # If the BDAY value is invalidly formatted, a feeble heuristic is applied # to find the month and year, and return a Date in the current year. def birthday value('BDAY') end # The CATEGORIES values, an array of String. A wrapper around # #value('CATEGORIES'). def categories value('CATEGORIES') end # The first EMAIL value of type +type+, a Email. Any of the location # attributes of Email can be used as +type+. A wrapper around # #value('EMAIL', +type+). def email(type=nil) value('EMAIL', type) end # The EMAIL values, an array of Email. If a block is given, the values are # yielded. A wrapper around #values('EMAIL'). def emails #:yield:email values('EMAIL') end # The GEO value, an Array of two Floats, +[ latitude, longitude]+. North # of the equator is positive latitude, east of the meridian is positive # longitude. See RFC2445 for more info, there are lots of special cases # and RFC2445's description is more complete thant RFC2426. def geo value('GEO') end # Return an Array of KEY Line#value, or yield each Line#value if a block # is given. A wrapper around #values('KEY'). # # KEY is a public key or authentication certificate associated with the # object that the vCard represents. It is not commonly used, but could # contain a X.509 or PGP certificate. # # See Attachment for a description of the value. def keys(&proc) #:yield: Line.value values('KEY', &proc) end # Return an Array of LOGO Line#value, or yield each Line#value if a block # is given. A wrapper around #values('LOGO'). # # LOGO is a graphic image of a logo associated with the object the vCard # represents. Its not common, but would probably be equivalent to the logo # on a printed card. # # See Attachment for a description of the value. def logos(&proc) #:yield: Line.value values('LOGO', &proc) end ## MAILER # The N and FN as a Name object. # # N is required for a vCards, this raises InvalidEncodingError if # there is no N so it cannot return nil. def name value('N') || raise(Vpim::InvalidEncodingError, "Missing mandatory N field") end # The first NICKNAME value, nil if there are none. def nickname v = value('NICKNAME') v = v.first if v v end # The NICKNAME values, an array of String. The array may be empty. def nicknames values('NICKNAME').flatten.uniq end # The NOTE value, a String. A wrapper around #value('NOTE'). def note value('NOTE') end # The ORG value, an Array of String. The first string is the organization, # subsequent strings are departments within the organization. A wrapper # around #value('ORG'). def org value('ORG') end # Return an Array of PHOTO Line#value, or yield each Line#value if a block # is given. A wrapper around #values('PHOTO'). # # PHOTO is an image or photograph information that annotates some aspect of # the object the vCard represents. Commonly there is one PHOTO, and it is a # photo of the person identified by the vCard. # # See Attachment for a description of the value. def photos(&proc) #:yield: Line.value values('PHOTO', &proc) end ## PRODID ## PROFILE ## REV ## ROLE # Return an Array of SOUND Line#value, or yield each Line#value if a block # is given. A wrapper around #values('SOUND'). # # SOUND is digital sound content information that annotates some aspect of # the vCard. By default this type is used to specify the proper # pronunciation of the name associated with the vCard. It is not commonly # used. Also, note that there is no mechanism available to specify that the # SOUND is being used for anything other than the default. # # See Attachment for a description of the value. def sounds(&proc) #:yield: Line.value values('SOUND', &proc) end ## SOURCE # The first TEL value of type +type+, a Telephone. Any of the location or # capability attributes of Telephone can be used as +type+. A wrapper around # #value('TEL', +type+). def telephone(type=nil) value('TEL', type) end # The TEL values, an array of Telephone. If a block is given, the values are # yielded. A wrapper around #values('TEL'). def telephones #:yield:tel values('TEL') end # The TITLE value, a text string specifying the job title, functional # position, or function of the object the card represents. A wrapper around # #value('TITLE'). def title value('TITLE') end ## UID # The URL value, a Attachment::Uri. A wrapper around #value('URL'). def url value('URL') end # The URL values, an Attachment::Uri. A wrapper around #values('URL'). def urls values('URL') end # The VERSION multiplied by 10 as an Integer. For example, a VERSION:2.1 # vCard would have a version of 21, and a VERSION:3.0 vCard would have a # version of 30. # # VERSION is required for a vCard, this raises InvalidEncodingError if # there is no VERSION so it cannot return nil. def version v = value('VERSION') unless v raise Vpim::InvalidEncodingError, 'Invalid vCard - it has no version field!' end v end # Make changes to a vCard. # # Yields a Vpim::Vcard::Maker that can be used to modify this vCard. def make #:yield: maker Vpim::Vcard::Maker.make2(self) do |maker| yield maker end end # Delete +line+ if block yields true. def delete_if #:nodoc: :yield: line # Do in two steps to not mess up progress through the enumerator. rm = [] each do |f| line = f2l(f) if line && yield(line) rm << f # Hack - because we treat N and FN as one field if f.name? 'N' rm << field('FN') end end end rm.each do |f| @fields.delete( f ) @cache.delete( f ) end end # A class to make and make changes to vCards. # # It can be used to create completely new vCards using Vcard#make2. # # Its is also yielded from Vpim::Vcard#make, in which case it allows a kind # of transactional approach to changing vCards, so their values can be # validated after any changes have been made. # # Examples: # - link:ex_mkvcard.txt: example of creating a vCard # - link:ex_cpvcard.txt: example of copying and them modifying a vCard # - link:ex_mkv21vcard.txt: example of creating version 2.1 vCard # - link:ex_mkyourown.txt: example of adding support for new fields to Vcard::Maker class Maker # Make a vCard. # # Yields +maker+, a Vpim::Vcard::Maker which allows fields to be added to # +card+, and returns +card+, a Vpim::Vcard. # # If +card+ is nil or not provided a new Vpim::Vcard is created and the # fields are added to it. # # Defaults: # - vCards must have both an N and an FN field, #make2 will fail if there # is no N field in the +card+ when your block is finished adding fields. # - If there is an N field, but no FN field, FN will be set from the # information in N, see Vcard::Name#preformatted for more information. # - vCards must have a VERSION field. If one does not exist when your block is # is finished it will be set to 3.0. def self.make2(card = Vpim::Vcard.create, &block) # :yields: maker new(nil, card).make(&block) end # Deprecated, use #make2. # # If set, the FN field will be set to +full_name+. Otherwise, FN will # be set from the values in #name. def self.make(full_name = nil, &block) # :yields: maker new(full_name, Vpim::Vcard.create).make(&block) end def make # :nodoc: yield self unless @card['N'] raise Unencodeable, 'N field is mandatory' end fn = @card.field('FN') if fn && fn.value.strip.length == 0 @card.delete(fn) fn = nil end unless fn @card << Vpim::DirectoryInfo::Field.create('FN', Vpim::Vcard::Name.new(@card['N'], '').formatted) end unless @card['VERSION'] @card << Vpim::DirectoryInfo::Field.create('VERSION', "3.0") end @card end private def initialize(full_name, card) # :nodoc: @card = card || Vpim::Vcard::create if full_name @card << Vpim::DirectoryInfo::Field.create('FN', full_name.strip ) end end public # Deprecated, see #name. # # Use # maker.name do |n| n.fullname = "foo" end # to set just fullname, or set the other fields to set fullname and the # name. def fullname=(fullname) #:nodoc: bacwards compat if @card.field('FN') raise Vpim::InvalidEncodingError, "Not allowed to add more than one FN field to a vCard." end @card << Vpim::DirectoryInfo::Field.create( 'FN', fullname ); end # Set the name fields, N and FN. # # Attributes of +name+ are: # - family: family name # - given: given name # - additional: additional names # - prefix: such as "Ms." or "Dr." # - suffix: such as "BFA", or "Sensei" # # +name+ is a Vcard::Name. # # All attributes are optional, though have all names be zero-length # strings isn't really in the spirit of things. FN's value will be set # to Vcard::Name#formatted if Vcard::Name#fullname isn't given a specific # value. # # Warning: This is the only mandatory field. def name #:yield:name x = begin @card.name.dup rescue Vpim::Vcard::Name.new end fn = x.fullname yield x x.fullname.strip! delete_if do |line| line.name == 'N' end @card << x.encode @card << x.encode_fn self end alias :add_name :name #:nodoc: backwards compatibility # Add an address field, ADR. +address+ is a Vpim::Vcard::Address. def add_addr # :yield: address x = Vpim::Vcard::Address.new yield x @card << x.encode self end # Add a telephone field, TEL. +tel+ is a Vpim::Vcard::Telephone. # # The block is optional, its only necessary if you want to specify # the optional attributes. def add_tel(number) # :yield: tel x = Vpim::Vcard::Telephone.new(number) if block_given? yield x end @card << x.encode self end # Add an email field, EMAIL. +email+ is a Vpim::Vcard::Email. # # The block is optional, its only necessary if you want to specify # the optional attributes. def add_email(email) # :yield: email x = Vpim::Vcard::Email.new(email) if block_given? yield x end @card << x.encode self end # Set the nickname field, NICKNAME. # # It can be set to a single String or an Array of String. def nickname=(nickname) delete_if { |l| l.name == 'NICKNAME' } @card << Vpim::DirectoryInfo::Field.create( 'NICKNAME', nickname ); end # Add a birthday field, BDAY. # # +birthday+ must be a time or date object. # # Warning: It may confuse both humans and software if you add multiple # birthdays. def birthday=(birthday) if !birthday.respond_to? :month raise ArgumentError, 'birthday must be a date or time object.' end delete_if { |l| l.name == 'BDAY' } @card << Vpim::DirectoryInfo::Field.create( 'BDAY', birthday ); end # Add a note field, NOTE. The +note+ String can contain newlines, they # will be escaped. def add_note(note) @card << Vpim::DirectoryInfo::Field.create( 'NOTE', Vpim.encode_text(note) ); end # Add an instant-messaging/point of presence address field, IMPP. The address # is a URL, with the syntax depending on the protocol. # # Attributes of IMPP are: # - preferred: true - set if this is the preferred address # - location: home, work, mobile - location of address # - purpose: personal,business - purpose of communications # # All attributes are optional, and so is the block. # # The URL syntaxes for the messaging schemes is fairly complicated, so I # don't try and build the URLs here, maybe in the future. This forces # the user to know the URL for their own address, hopefully not too much # of a burden. # # IMPP is defined in draft-jennings-impp-vcard-04.txt. It refers to the # URI scheme of a number of messaging protocols, but doesn't give # references to all of them: # - "xmpp" indicates to use XMPP, draft-saintandre-xmpp-uri-06.txt # - "irc" or "ircs" indicates to use IRC, draft-butcher-irc-url-04.txt # - "sip" indicates to use SIP/SIMPLE, RFC 3261 # - "im" or "pres" indicates to use a CPIM or CPP gateway, RFC 3860 and RFC 3859 # - "ymsgr" indicates to use yahoo # - "msn" might indicate to use Microsoft messenger # - "aim" indicates to use AOL # def add_impp(url) # :yield: impp params = {} if block_given? x = Struct.new( :location, :preferred, :purpose ).new yield x x[:preferred] = 'PREF' if x[:preferred] types = x.to_a.flatten.compact.map { |s| s.downcase }.uniq params['TYPE'] = types if types.first end @card << Vpim::DirectoryInfo::Field.create( 'IMPP', url, params) self end # Add an X-AIM account name where +xaim+ is an AIM screen name. # # I don't know if this is conventional, or supported by anything other # than AddressBook.app, but an example is: # X-AIM;type=HOME;type=pref:exampleaccount # # Attributes of X-AIM are: # - preferred: true - set if this is the preferred address # - location: home, work, mobile - location of address # # All attributes are optional, and so is the block. def add_x_aim(xaim) # :yield: xaim params = {} if block_given? x = Struct.new( :location, :preferred ).new yield x x[:preferred] = 'PREF' if x[:preferred] types = x.to_a.flatten.compact.map { |s| s.downcase }.uniq params['TYPE'] = types if types.first end @card << Vpim::DirectoryInfo::Field.create( 'X-AIM', xaim, params) self end # Add a photo field, PHOTO. # # Attributes of PHOTO are: # - image: set to image data to include inline # - link: set to the URL of the image data # - type: string identifying the image type, supposed to be an "IANA registered image format", # or a non-registered image format (usually these start with an x-) # # An error will be raised if neither image or link is set, or if both image # and link is set. # # Setting type is optional for a link image, because either the URL, the # image file extension, or a HTTP Content-Type may specify the type. If # it's not a link, setting type is mandatory, though it can be set to an # empty string, '', if the type is unknown. # # TODO - I'm not sure about this API. I'm thinking maybe it should be # #add_photo(image, type), and that I should detect when the image is a # URL, and make type mandatory if it wasn't a URL. def add_photo # :yield: photo x = Struct.new(:image, :link, :type).new yield x if x[:image] && x[:link] raise Vpim::InvalidEncodingError, 'Image is not allowed to be both inline and a link.' end value = x[:image] || x[:link] if !value raise Vpim::InvalidEncodingError, 'A image link or inline data must be provided.' end params = {} # Don't set type to the empty string. params['TYPE'] = x[:type] if( x[:type] && x[:type].length > 0 ) if x[:link] params['VALUE'] = 'URI' else # it's inline, base-64 encode it params['ENCODING'] = :b64 if !x[:type] raise Vpim::InvalidEncodingError, 'Inline image data must have it\'s type set.' end end @card << Vpim::DirectoryInfo::Field.create( 'PHOTO', value, params ) self end # Set the title field, TITLE. # # It can be set to a single String. def title=(title) delete_if { |l| l.name == 'TITLE' } @card << Vpim::DirectoryInfo::Field.create( 'TITLE', title ); end # Set the org field, ORG. # # It can be set to a single String or an Array of String. def org=(org) delete_if { |l| l.name == 'ORG' } @card << Vpim::DirectoryInfo::Field.create( 'ORG', org ); end # Add a URL field, URL. def add_url(url) @card << Vpim::DirectoryInfo::Field.create( 'URL', url.to_str ); end # Add a Field, +field+. def add_field(field) fieldname = field.name.upcase case when [ 'BEGIN', 'END' ].include?(fieldname) raise Vpim::InvalidEncodingError, "Not allowed to manually add #{field.name} to a vCard." when [ 'VERSION', 'N', 'FN' ].include?(fieldname) if @card.field(fieldname) raise Vpim::InvalidEncodingError, "Not allowed to add more than one #{fieldname} to a vCard." end @card << field else @card << field end end # Copy the fields from +card+ into self using #add_field. If a block is # provided, each Field from +card+ is yielded. The block should return a # Field to add, or nil. The Field doesn't have to be the one yielded, # allowing the field to be copied and modified (see Field#copy) before adding, or # not added at all if the block yields nil. # # The vCard fields BEGIN and END aren't copied, and VERSION, N, and FN are copied # only if the card doesn't have them already. def copy(card) # :yields: Field card.each do |field| fieldname = field.name.upcase case when [ 'BEGIN', 'END' ].include?(fieldname) # Never copy these when [ 'VERSION', 'N', 'FN' ].include?(fieldname) && @card.field(fieldname) # Copy these only if they don't already exist. else if block_given? field = yield field end if field add_field(field) end end end end # Delete +line+ if block yields true. def delete_if #:yield: line begin @card.delete_if do |line| yield line end rescue NoMethodError # FIXME - this is a hideous hack, allowing a DirectoryInfo to # be passed instead of a Vcard, and for it to almost work. Yuck. end end end end end vpim-0.695/lib/vpim/version.rb0000644000076500000240000000057111152674120014710 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end module Vpim PRODID = '-//Ensemble Independent//vPim 0.695//EN' VERSION = '0.695' # Return the API version as a string. def Vpim.version VERSION end end vpim-0.695/lib/vpim/vevent.rb0000644000076500000240000001353511152674120014536 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require 'vpim/dirinfo' require 'vpim/field' require 'vpim/rfc2425' require 'vpim/rrule' require 'vpim/vpim' require 'vpim/property/base' require 'vpim/property/common' require 'vpim/property/priority' require 'vpim/property/location' require 'vpim/property/resources' require 'vpim/property/recurrence' module Vpim class Icalendar class Vevent include Vpim::Icalendar::Property::Base include Vpim::Icalendar::Property::Common include Vpim::Icalendar::Property::Priority include Vpim::Icalendar::Property::Location include Vpim::Icalendar::Property::Resources include Vpim::Icalendar::Property::Recurrence def initialize(fields) #:nodoc: outer, inner = Vpim.outer_inner(fields) @properties = Vpim::DirectoryInfo.create(outer) @elements = inner # See "TODO - fields" in dirinfo.rb end # TODO - derive everything from Icalendar::Component to get rid of this kind of stuff? def fields #:nodoc: f = @properties.to_a last = f.pop f.push @elements f.push last end def properties #:nodoc: @properties end # Create a new Vevent object. All events must have a DTSTART field, # specify it as either a Time or a Date in +start+, it defaults to "now" # # If specified, +fields+ must be either an array of Field objects to # add, or a Hash of String names to values that will be used to build # Field objects. The latter is a convenient short-cut allowing the Field # objects to be created for you when called like: # # Vevent.create(Date.today, 'SUMMARY' => "today's event") # def Vevent.create(start = Time.now, fields=[]) # TODO # - maybe events are usually created in a particular way? With a # start/duration or a start/end? Maybe I can make it easier. Ideally, I # would like to make it hard to encode an invalid Event. # - I don't think its useful to have a default dtstart for events # - also, I don't think dstart is mandatory dtstart = DirectoryInfo::Field.create('DTSTART', start) di = DirectoryInfo.create([ dtstart ], 'VEVENT') Vpim::DirectoryInfo::Field.create_array(fields).each { |f| di.push_unique f } new(di.to_a) end # Creates a yearly repeating event, such as for a birthday. def Vevent.create_yearly(date, summary) create( date, 'SUMMARY' => summary.to_str, 'RRULE' => 'FREQ=YEARLY' ) end # Accept an event invitation. The +invitee+ is the Address that wishes # to accept the event invitation as confirmed. # # The event created is identical to this one, but # - without the attendees # - with the invitee added with a PARTSTAT of ACCEPTED def accept(invitee) # FIXME - move to Vpim::Itip. invitee = invitee.copy invitee.partstat = 'ACCEPTED' fields = [] @properties.each_with_index do |f,i| # put invitee in as field[1] fields << invitee.encode('ATTENDEE') if i == 1 fields << f unless f.name? 'ATTENDEE' end Vevent.new(fields) end # In iTIP, whether this event is OPAQUE or TRANSPARENT to scheduling. If # transparency is not explicitly set, it defaults to OPAQUE. def transparency proptoken 'TRANSP', ["OPAQUE", "TRANSPARENT"], "OPAQUE" end # The duration in seconds of an Event, or nil if unspecified. If the # DURATION field is not present, but the DTEND field is, the duration is # calculated from DTSTART and DTEND. Durations of zero seconds are # possible. def duration propduration 'DTEND' end # The end time for this Event. If the DTEND field is not present, but the # DURATION field is, the end will be calculated from DTSTART and # DURATION. def dtend propend 'DTEND' end # Make a new Vevent, or make changes to an existing Vevent. class Maker include Vpim::Icalendar::Set::Util #:nodoc: include Vpim::Icalendar::Set::Common # The event that changes are being made to. attr_reader :event def initialize(event) #:nodoc: @event = event @comp = event end # Make changes to +event+. If +event+ is not specified, creates a new # event. Yields a Vevent::Maker, and returns +event+. def self.make(event = Vpim::Icalendar::Vevent.create) #:yield:maker m = self.new(event) yield m m.event end # Set transparency to "OPAQUE" or "TRANSPARENT", see Vpim::Vevent#transparency. def transparency(token) set_token 'TRANSP', ["OPAQUE", "TRANSPARENT"], "OPAQUE", token end # Set end for events with fixed durations. +end+ can be a Date or Time def dtend(dtend) set_date_or_datetime 'DTEND', 'DATE-TIME', dtend end # Add a RRULE to this event. The rule can be provided as a pre-build # RRULE value, or the RRULE maker can be used. def add_rrule(rule = nil, &block) #:yield: Rrule::Maker # TODO - should be in Property::Reccurrence::Set unless rule rule = Rrule::Maker.new(&block).encode end @comp.properties.push(Vpim::DirectoryInfo::Field.create("RRULE", rule)) self end # Set the RRULE for this event. See #add_rrule def set_rrule(rule = nil, &block) #:yield: Rrule::Maker rm_all("RRULE") add_rrule(rule, &block) end end end end end vpim-0.695/lib/vpim/view.rb0000644000076500000240000000451511152674120014177 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require "enumerator" module Vpim module View SECSPERDAY = 24 * 60 * 60 # View only events occuring in the next week. module Week def each(klass = nil) #:nodoc: unless block_given? return Enumerable::Enumerator.new(self, :each, klass) end t0 = Time.new.to_a t0[0] = t0[1] = t0[2] = 0 # sec,min,hour = 0 t0 = Time.local(*t0) t1 = t0 + 7 * SECSPERDAY # Need to filter occurrences, too. Create modules for this on the fly. occurrences = Module.new # I'm passing state into the module's instance methods by doing string # evaluation... which sucks, but I don't think I can get this closure in # there. occurrences.module_eval(<<"__", __FILE__, __LINE__+1) def occurrences(dountil=nil) unless block_given? return Enumerable::Enumerator.new(self, :occurrences, dountil) end super(dountil) do |t| t0 = Time.at(#{t0.to_i}) t1 = Time.at(#{t1.to_i}) break if t >= t1 tend = t if respond_to? :duration tend += duration || 0 end if tend >= t0 yield t end end end __ =begin block = lambda do |dountil| unless block_given? return Enumerable::Enumerator.new(self, :occurrences, dountil) end super(dountil) do |t| break if t >= t1 yield t end end occurrences.send(:define_method, :occurrences, block) =end super do |ve| if ve.occurs_in?(t0, t1) if ve.respond_to? :occurrences ve.extend occurrences end yield ve end end end end # Return a calendar view for the next week. def self.week(cal) cal.clone.extend Week.dup end module Todo end # Return a calendar view of only todos (optionally, include todos that # are done). def self.todos(cal, withdone=false) end end end vpim-0.695/lib/vpim/vjournal.rb0000644000076500000240000000244311152674120015063 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require 'vpim/dirinfo' require 'vpim/field' require 'vpim/rfc2425' require 'vpim/vpim' require 'vpim/property/base' require 'vpim/property/common' require 'vpim/property/recurrence' module Vpim class Icalendar class Vjournal include Vpim::Icalendar::Property::Base include Vpim::Icalendar::Property::Common include Vpim::Icalendar::Property::Recurrence def initialize(fields) #:nodoc: outer, inner = Vpim.outer_inner(fields) @properties = Vpim::DirectoryInfo.create(outer) @elements = inner end # TODO - derive everything from Icalendar::Component to get rid of this kind of stuff? def fields #:nodoc: f = properties.to_a last = f.pop f.push @elements f.push last end def properties #:nodoc: @properties end # Create a Vjournal component. def self.create(fields=[]) di = DirectoryInfo.create([], 'VJOURNAL') Vpim::DirectoryInfo::Field.create_array(fields).each { |f| di.push_unique f } new(di.to_a) end end end end vpim-0.695/lib/vpim/vpim.rb0000644000076500000240000000345311152674120014200 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require 'vpim/version' #:main:README #:title:vPim - vCard and iCalendar support for Ruby module Vpim # Exception used to indicate that data being decoded is invalid, the message # should describe what is invalid. class InvalidEncodingError < StandardError; end # Exception used to indicate that data being decoded is unsupported, the message # should describe what is unsupported. # # If its unsupported, its likely because I didn't anticipate it being useful # to support this, and it likely it could be supported on request. class UnsupportedError < StandardError; end # Exception used to indicate that encoding failed, probably because the # object would not result in validly encoded data. The message should # describe what is unsupported. class Unencodeable < StandardError; end end module Vpim::Methods #:nodoc: module_function # Case-insensitive comparison of +str0+ to +str1+, returns true or false. # Either argument can be nil, where nil compares not equal to anything other # than nil. # # This is available both as a module function: # Vpim::Methods.casecmp?("yes", "YES") # and an instance method: # include Vpim::Methods # casecmp?("yes", "YES") # # Will work with ruby1.6 and ruby 1.8. # # TODO - could make this be more efficient, but I'm supporting 1.6, not # optimizing for it. def casecmp?(str0, str1) if str0 == nil if str1 == nil return true else return false end end begin str0.casecmp(str1) == 0 rescue NoMethodError str0.downcase == str1.downcase end end end vpim-0.695/lib/vpim/vtodo.rb0000644000076500000240000000601111152674120014351 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end require 'vpim/dirinfo' require 'vpim/field' require 'vpim/rfc2425' require 'vpim/vpim' require 'vpim/property/base' require 'vpim/property/common' require 'vpim/property/priority' require 'vpim/property/location' require 'vpim/property/resources' require 'vpim/property/recurrence' module Vpim class Icalendar class Vtodo include Vpim::Icalendar::Property::Base include Vpim::Icalendar::Property::Common include Vpim::Icalendar::Property::Priority include Vpim::Icalendar::Property::Location include Vpim::Icalendar::Property::Resources include Vpim::Icalendar::Property::Recurrence def initialize(fields) #:nodoc: outer, inner = Vpim.outer_inner(fields) @properties = Vpim::DirectoryInfo.create(outer) @elements = inner end # TODO - derive everything from Icalendar::Component to get this kind of stuff? def fields #:nodoc: f = @properties.to_a last = f.pop f.push @elements f.push last end def properties #:nodoc: @properties end # Create a new Vtodo object. # # If specified, +fields+ must be either an array of Field objects to # add, or a Hash of String names to values that will be used to build # Field objects. The latter is a convenient short-cut allowing the Field # objects to be created for you when called like: # # Vtodo.create('SUMMARY' => "buy mangos") # # TODO - maybe todos are usually created in a particular way? I can # make it easier. Ideally, I would like to make it hard to encode an invalid # Event. def Vtodo.create(fields=[]) di = DirectoryInfo.create([], 'VTODO') Vpim::DirectoryInfo::Field.create_array(fields).each { |f| di.push_unique f } new(di.to_a) end # The duration in seconds of a Todo, or nil if unspecified. If the # DURATION field is not present, but the DUE field is, the duration is # calculated from DTSTART and DUE. Durations of zero seconds are # possible. def duration propduration 'DUE' end # The time at which this Todo is due to be completed. If the DUE field is not present, # but the DURATION field is, due will be calculated from DTSTART and DURATION. def due propend 'DUE' end # The date and time that a to-do was actually completed, a Time. def completed proptime 'COMPLETED' end # The percentage completetion of the to-do, between 0 and 100. 0 means it hasn't # started, 100 that it has been completed. # # TODO - the handling of this property isn't tied to either COMPLETED: or # STATUS:, but perhaps it should be? def percent_complete propinteger 'PERCENT-COMPLETE' end end end end vpim-0.695/lib/vpim.rb0000644000076500000240000000050511152674120013220 0ustar samstaff=begin Copyright (C) 2008 Sam Roberts This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end # the existence of this file is a hack to support users or rubygems require 'vpim/icalendar' require 'vpim/vcard' vpim-0.695/README0000644000076500000240000001340511152674120012035 0ustar samstaffAuthor:: Sam Roberts Copyright:: Copyright (C) 2008 Sam Roberts License:: May be distributed under the same terms as Ruby Homepage:: http://vpim.rubyforge.org Download:: http://rubyforge.org/projects/vpim Install:: sudo gem install vpim vPim provides calendaring, scheduling, and contact support for Ruby through the standard iCalendar and vCard data formats for "personal information" exchange. = Thanks - http://ZipDX.com: for sponsoring development of FREQ=weekly and BYSETPOS in recurrence rules. - http://RubyForge.org: for their generous hosting of this project. = Installation There is a vPim package installable using ruby-gems: # sudo gem install vpim (may require root privilege) It is also installable in the standard way. Untar the package, and do: $ ruby setup.rb --help or do: $ ruby setup.rb config $ ruby setup.rb setup # ruby setup.rb install (may require root privilege) = Overview vCard (RFC 2426) is a format for personal information, see Vpim::Vcard and Vpim::Maker::Vcard. iCalendar (RFC 2445) is a format for calendar related information, see Vpim::Icalendar. vCard and iCalendar support is built on top of an implementation of the MIME Content-Type for Directory Information (RFC 2425). The basic RFC 2425 format is implemented by Vpim::DirectoryInfo and Vpim::DirectoryInfo::Field. The libary is quite useful, but improvements are ongoing. If you find something missing or have suggestions, please contact me. I can't promise instantaneous turnaround, but I might be able to suggest another approach, and features requested by users of vPim go to the top of the todo list. If you need a feature for a commercial project, consider sponsoring development. = Examples Here's an example to give a sense for how iCalendars are encoded and decoded: require 'vpim/icalendar' cal = Vpim::Icalendar.create2 cal.add_event do |e| e.dtstart Date.new(2005, 04, 28) e.dtend Date.new(2005, 04, 29) e.summary "Monthly meet-the-CEO day" e.description <<'---' Unlike last one, this meeting will change your life because we are going to discuss your likely demotion if your work isn't done soon. --- e.categories [ 'APPOINTMENT' ] e.categories do |c| c.push 'EDUCATION' end e.url 'http://www.example.com' e.sequence 0 e.access_class "PRIVATE" e.transparency 'OPAQUE' now = Time.now e.created now e.lastmod now e.organizer do |o| o.cn = "Example Organizer, Mr." o.uri = "mailto:organizer@example.com" end attendee = Vpim::Icalendar::Address.create("mailto:attendee@example.com") attendee.rsvp = true e.add_attendee attendee end icsfile = cal.encode puts '--- Encode:' puts icsfile puts '--- Decode:' cal = Vpim::Icalendar.decode(icsfile).first cal.components do |e| puts e.summary puts e.description puts e.dtstart.to_s puts e.dtend.to_s end This produces: --- Encode: BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Ensemble Independent//vPim 0.357//EN CALSCALE:Gregorian BEGIN:VEVENT DTSTART;VALUE=DATE:20050428 DTEND;VALUE=DATE:20050429 SUMMARY:Monthly meet-the-CEO day DESCRIPTION:Unlike last one, this meeting will change your life because\nwe are going to discuss your likely demotion if your work isn't\ndone soon.\n CATEGORIES:APPOINTMENT,EDUCATION URL:http://www.example.com SEQUENCE:0 CLASS:PRIVATE CREATED:20060402T231755 LAST-MODIFIED:20060402T231755 ORGANIZER;CN="Example Organizer, Mr.":mailto:organizer@example.com ATTENDEE;RSVP=true:mailto:attendee@example.com END:VEVENT END:VCALENDAR --- Decode: Monthly meet-the-CEO day Unlike last one, this meeting will change your life because we are going to discuss your likely demotion if your work isn't done soon. Thu Apr 28 00:00:00 UTC 2005 Fri Apr 29 00:00:00 UTC 2005 More examples of using vPim are provided in samples/. vCard examples are: - link:ex_mkvcard.txt: example of creating a vCard - link:ex_cpvcard.txt: example of copying and them modifying a vCard - link:ex_mkv21vcard.txt: example of creating version 2.1 vCard - link:mutt-aliases-to-vcf.txt: convert a mutt aliases file to vCards - link:ex_get_vcard_photo.txt: pull photo data from a vCard - link:ab-query.txt: query the OS X Address Book to find vCards - link:vcf-to-mutt.txt: query vCards for matches, output in formats useful with Mutt (see link:README.mutt for details) - link:tabbed-file-to-vcf.txt: convert a tab-delimited file to vCards, a (small but) complete application contributed by Dane G. Avilla, thanks! - link:vcf-to-ics.txt: example of how to create calendars of birthdays from vCards - link:vcf-dump.txt: utility for dumping contents of .vcf files iCalendar examples are: - link:ics-to-rss.txt: prints todos as RSS, or starts a WEBrick servlet that publishes todos as a RSS feed. Thanks to Dave Thomas for this idea, from http://pragprog.com/pragdave/Tech/Blog/ToDos.rdoc. - link:cmd-itip.txt: prints emailed iCalendar invitations in human-readable form, and see link:README.mutt for instruction on mutt integration. I get a lot of meeting invitations from Lotus Notes/Domino users at work, and this is pretty useful in figuring out where and when I am supposed to be. - link:reminder.txt: prints upcoming events and todos, by default from Apple's iCal calendars - link:rrule.txt: utility for printing recurrence rules - link:ics-dump.txt: utility for dumping contents of .ics files = Project Information vPim can be downloaded from the Ruby Forge project page: - http://rubyforge.org/projects/vpim or installed as a gem: - sudo gem install vpim For notifications about new releases, or to ask questions about vPim, please subscribe to "vpim-talk": - http://rubyforge.org/mailman/listinfo/vpim-talk vpim-0.695/samples/0000755000076500000240000000000011152674120012616 5ustar samstaffvpim-0.695/samples/ab-query.rb0000644000076500000240000000174611152674120014700 0ustar samstaff#!/usr/bin/env ruby $-w = true $:.unshift File.dirname($0) + '/../lib' require 'osx-wrappers' require 'getoptlong' require 'vpim/vcard' require 'osx-wrappers' HELP =< Options -h,--help Print this helpful message. -d,--debug Print debug information. -m,--my-addrs My email addresses, a REGEX. Examples: EOF opt_debug = nil opt_print = true # Ways to get this: # Use a --mutt option, and steal it from muttrc, # from $USER, $LOGNAME,, from /etc/passwd... opt_myaddrs = nil opts = GetoptLong.new( [ "--help", "-h", GetoptLong::NO_ARGUMENT ], [ "--myaddrs", "-m", GetoptLong::REQUIRED_ARGUMENT ], [ "--accept", "-a", GetoptLong::REQUIRED_ARGUMENT ], [ "--reject", "-r", GetoptLong::REQUIRED_ARGUMENT ], [ "--debug", "-d", GetoptLong::NO_ARGUMENT ] ) opts.each do |opt, arg| case opt when "--help" then puts HELP exit 0 when "--debug" then require 'pp' opt_debug = true when "--myaddrs" then opt_myaddrs = Regexp.new(arg, 'i') end end if ARGV.length < 1 puts "no input files specified, try -h!\n" exit 1 end ARGV.each do |file| cals = Vpim::Icalendar.decode(File.open(file)) cals.each do |cal| if opt_debug puts "vCalendar: version=#{cal.version/10.0} producer='#{cal.producer}'" if cal.protocol; puts " protocol-method=#{cal.protocol}"; end end events = cal.events if events.size != 1 raise "!! #{events.size} calendar events is more than 1!" end events.each do |e| summary = e.summary || e.comment || '' case cal.protocol.upcase when 'PUBLISH' puts "Notification of: #{summary}" when 'REQUEST' puts "Request for: #{summary}" when 'REPLY' else raise "!! unhandled protocol type #{cal.protocol}!" end puts "Organized by: #{e.organizer.to_s}" # TODO - spec as hours/mins/secs e.occurrences.each_with_index do |t, i| if(i < 1) puts "At time: #{t}" +( e.duration ? " for #{Duration.secs(e.duration).to_s}" : '' ) else puts "... and others" break end end if e.location; puts "Located at: #{e.location}"; end if e.description puts finish="-- Description --" puts e.description end if e.comments puts finish="-- Comment --" puts " comment=#{e.comments}" end if e.attendees.first puts finish="-- Attendees --" e.attendees.each_with_index do |a,i| puts "#{i} #{a.to_s}" if !opt_myaddrs || a.uri =~ opt_myaddrs puts " participation-status: #{a.partstat ? a.partstat.downcase : 'unknown'}" puts " response-requested? #{a.rsvp ? 'yes' : 'no'}" end end end if finish puts '-' * finish.length end if opt_debug if e.status; puts " status=#{e.status}"; end puts " uid=#{e.uid}" puts " dtstamp=#{e.dtstamp.to_s}" puts " dtstart=#{e.dtstart.to_s}" if e.dtend; puts " dtend=#{e.dtend.to_s}"; end if e.rrule; puts " rrule=#{e.rrule}"; end end end todos = cal.todos todos.each do |e| s = e.status ? " (#{e.status})" : '' puts "Todo#{s}: #{e.summary}" end if opt_debug pp cals end end end vpim-0.695/samples/ex_cpvcard.rb0000644000076500000240000000214711152674120015265 0ustar samstaffrequire 'vpim/vcard' ORIGINAL =<<'---' BEGIN:VCARD VERSION:3.0 FN:Jimmy Death N:Death;Jimmy;;Dr.; TEL:+416 123 1111 TEL;type=home,pref:+416 123 2222 TEL;type=work,fax:+416+123+3333 EMAIL;type=work:drdeath@work.com EMAIL;type=pref:drdeath@home.net NOTE:Do not call. END:VCARD --- original = Vpim::Vcard.decode(ORIGINAL).first puts original modified = Vpim::Vcard::Maker.make2 do |maker| # Set the fullname field to use family-given name order. maker.name do |n| n.fullname = "#{original.name.family} #{original.name.given}" end # Copy original fields, with some changes: # - set only work email addresses and telephone numbers to be preferred. # - don't copy notes maker.copy(original) do |field| if field.name? 'EMAIL' field = field.copy field.pref = field.type? 'work' end if field.name? 'TEL' field = field.copy field.pref = field.type? 'work' end if field.name? 'NOTE' field = nil end field end end puts '---' puts modified Vpim::Vcard::Maker.make2(modified) do |maker| maker.nickname = "Your Last Friend" end puts '---' puts modified vpim-0.695/samples/ex_get_vcard_photo.rb0000644000076500000240000000071611152674120017012 0ustar samstaff#!/usr/bin/ruby -w require 'vpim/vcard' vcf = open(ARGV[0] || 'data/vcf/Sam Roberts.vcf') card = Vpim::Vcard.decode(vcf).first card.photos.each_with_index do |photo, i| file = "_photo_#{i}." if photo.format file += photo.format.gsub('/', '_') else # You are your own if PHOTO doesn't include a format. AddressBook.app # exports TIFF, for example, but doesn't specify that. file += 'tiff' end open(file, 'w').write photo.to_s end vpim-0.695/samples/ex_mkv21vcard.rb0000644000076500000240000000155611152674120015626 0ustar samstaff# Note that while most version 3.0 vCards should be valid 2.1 vCards, they # aren't guaranteed to be. vCard 2.1 is reasonably well supported on decode, # I'm not sure how well it works on encode. # # Most things should work, but you should test whether this works with your 2.1 # vCard decoder. Also, avoid base64 encoding, or do it manually. require 'vpim/vcard' # Create a new 2.1 vCard. card21 = Vpim::DirectoryInfo.create( [ Vpim::DirectoryInfo::Field.create('VERSION', '2.1') ], 'VCARD') Vpim::Vcard::Maker.make2(card21) do |maker| maker.name do |n| n.prefix = 'Dr.' n.given = 'Jimmy' n.family = 'Death' end end puts card21 # Copy and modify a 2.1 vCard, preserving it's version. mod21 = Vpim::Vcard::Maker.make2(Vpim::DirectoryInfo.create([], 'VCARD')) do |maker| maker.copy card21 maker.nickname = 'some name' end puts '---' puts mod21 vpim-0.695/samples/ex_mkvcard.rb0000644000076500000240000000266611152674120015300 0ustar samstaffrequire 'vpim/vcard' card = Vpim::Vcard::Maker.make2 do |maker| maker.add_name do |name| name.prefix = 'Dr.' name.given = 'Jimmy' name.family = 'Death' end maker.add_addr do |addr| addr.preferred = true addr.location = 'work' addr.street = '12 Last Row, 13th Section' addr.locality = 'City of Lost Children' addr.country = 'Cinema' end maker.add_addr do |addr| addr.location = [ 'home', 'zoo' ] addr.delivery = [ 'snail', 'stork', 'camel' ] addr.street = '12 Last Row, 13th Section' addr.locality = 'City of Lost Children' addr.country = 'Cinema' end maker.nickname = "The Good Doctor" maker.birthday = Date.today maker.add_photo do |photo| photo.link = 'http://example.com/image.png' end maker.add_photo do |photo| photo.image = "File.open('drdeath.jpg').read # a fake string, real data is too large :-)" photo.type = 'jpeg' end maker.add_tel('416 123 1111') maker.add_tel('416 123 2222') { |t| t.location = 'home'; t.preferred = true } maker.add_impp('joe') do |impp| impp.preferred = 'yes' impp.location = 'mobile' end maker.add_x_aim('example') do |xaim| xaim.location = 'row12' end maker.add_tel('416-123-3333') do |tel| tel.location = 'work' tel.capability = 'fax' end maker.add_email('drdeath@work.com') { |e| e.location = 'work' } maker.add_email('drdeath@home.net') { |e| e.preferred = 'yes' } end puts card vpim-0.695/samples/ex_mkyourown.rb0000644000076500000240000000121111152674120015704 0ustar samstaffrequire 'vpim/vcard' module Vpim class Vcard class Maker # Add a user-defined field, X-MY-OWN:. # # This can be done both to encode custom fields, or to add support for # fields that Vcard::Maker doesn't support. In the latter case, please # submit your methods so I can add them to vPim. def add_my_own(value) @card << Vpim::DirectoryInfo::Field.create( 'X-MY-OWN', value.to_str ); end end end end card = Vpim::Vcard.create # ... or load from somewhere Vpim::Vcard::Maker.make2(card) do |m| m.add_name do |n| n.given = 'Given' end m.add_my_own 'my value' end puts card vpim-0.695/samples/ics-dump.rb0000755000076500000240000000743111152674120014674 0ustar samstaff#!/usr/bin/env ruby # # Calendars are in ~/Library/Calendars/ $-w = true $:.unshift File.dirname($0) + '/../lib' require 'getoptlong' require 'pp' require 'open-uri' require 'vpim/icalendar' require 'vpim/duration' include Vpim HELP =<... Options -h,--help Print this helpful message. -n,--node Dump as nodes. -d,--debug Print debug information. -m,--metro Convert metro. Examples: EOF opt_debug = nil opt_node = false opts = GetoptLong.new( [ "--help", "-h", GetoptLong::NO_ARGUMENT ], [ "--node", "-n", GetoptLong::NO_ARGUMENT ], [ "--debug", "-d", GetoptLong::NO_ARGUMENT ] ) opts.each do |opt, arg| case opt when "--help" then puts HELP exit 0 when "--node" then opt_node = true when "--debug" then opt_debug = true end end if ARGV.length < 1 puts "no input files specified, try -h!\n" exit 1 end if opt_node ARGV.each do |file| tree = Vpim.expand(Vpim.decode(File.open(file).read(nil))) pp tree end exit 0 end def v2s(v) case v when Vpim::Icalendar::Attachment if v.binary "#{v.format.inspect} binary #{v.binary.inspect}" else s = "#{v.format.inspect} uri #{v.uri.inspect}" begin s << " #{v.value.gets.inspect}..." rescue s << " (#{$!.class})" end end else v #.inspect end end def puts_properties(c) [ :access_class, :attachments, :categories, :comments, :completed, :contacts, :created, :description, :dtend, :dtstamp, :dtstart, :due, :geo, :location, :organizer, :percent_complete, :priority, :sequence, :status, :summary, :transparency, :uid, :url, ].each do |m| if c.respond_to? m v = c.send(m) case v when Array v.each_with_index do |v,i| puts " #{m}[#{i}]=<#{v2s v}>" end else if v puts " #{m}=<#{v2s v}>" end end end end begin if c.duration; puts " duration=#{Duration.secs(c.duration).to_s}"; end rescue NoMethodError end c.attendees.each_with_index do |a,i| puts " attendee[#{i}]=#{a.to_s}" puts " role=#{a.role.upcase} participation-status=#{a.partstat.upcase} rsvp?=#{a.rsvp ? 'yes' : 'no'}" end [ 'RRULE', 'RDATE', 'EXRULE', 'EXDATE', ].each do |m| c.propvaluearray(m).each_with_index do |v,i| puts " #{m}[#{i}]=<#{v.to_s}>" case when i == 1 && m != 'RRULE' # Anything that isn't an RRULE isn't supported at all. puts " ==> #{m} is unsupported!" when i == 2 && m == 'RRULE' # If there was more than 1 RRULE, its not supported. puts " ==> More than one RRULE is unsupported!" end end end begin c.occurrences.each_with_index do |t, i| if(i < 10) puts " #{i+1} -> #{t}" else puts " ..." break; end end rescue ArgumentError # No occurrences. end end ARGV.each do |file| puts "===> ", file cals = Vpim::Icalendar.decode( (file == "-") ? $stdin : open(file) ) cals.each_with_index do |cal, i| if i > 0 puts end puts "Icalendar[#{i}]:" puts " version=#{cal.version/10.0}" puts " producer=#{cal.producer}" if cal.protocol; puts " protocol=#{cal.protocol}"; end events = cal.events cal.components.each_with_index do |c, i| puts " #{c.class.to_s.sub(/.*::/,'')}[#{i}]:" begin puts_properties(c) rescue => e cb = e.backtrace pp e print cb.shift, ":", e.message, " (", e.class, ")\n" cb.each{|c| print "\tfrom ", c, "\n"} exit 1 end end if opt_debug pp cals end end end vpim-0.695/samples/ics-to-rss.rb0000755000076500000240000000411711152674120015154 0ustar samstaff#!/usr/bin/env ruby # # Call with --print to print RSS to stdout, otherwise it runs as a WEBrick # servelet on port 8080. # # This comes from an idea of Dave Thomas' that he described here: # # http://pragprog.com/pragdave/Tech/Blog/ToDos.rdoc # # He generously sent me his code, and I reimplemented it with vPim and rss/maker. # # RSS Content-Types: # # RSS 1.0 -> application/rdf+xml # RSS 2.0 -> text/xml # RSS 0.9 -> text/xml # ATOM -> application/xml require 'rss/maker' require 'vpim/icalendar' class IcalToRss def initialize(calendars, title, link, language = 'en-us') @rss = RSS::Maker.make("0.9") do |maker| maker.channel.title = title maker.channel.link = link maker.channel.description = title maker.channel.language = language # These are required, or RSS::Maker silently returns nil! maker.image.url = "maker.image.url" maker.image.title = "maker.image.title" calendars.each do |file| Vpim::Icalendar.decode(File.open(file)).each do |cal| cal.todos.each do |todo| if !todo.status || todo.status.upcase != "COMPLETED" item = maker.items.new_item item.title = todo.summary item.link = todo.properties['url'] || link item.description = todo.description || todo.summary end end end end end end def to_rss @rss.to_s end end TITLE = "Sam's ToDo List" LINK = "http://ensemble.local/~sam" if ARGV[0] == "--print" puts IcalToRss.new( Dir[ "/Users/sam/Library/Calendars/*.ics" ], TITLE, LINK ).to_rss else require 'webrick' class IcalRssTodoServlet < WEBrick::HTTPServlet::AbstractServlet def do_GET(req, resp) resp.body = IcalToRss.new( Dir[ "/Users/sam/Library/Calendars/*.ics" ], TITLE, LINK ).to_rss resp['content-type'] = 'text/xml' raise WEBrick::HTTPStatus::OK end end server = WEBrick::HTTPServer.new( :Port => 8080 ) server.mount( '/', IcalRssTodoServlet ) ['INT', 'TERM'].each { |signal| trap(signal) { server.shutdown } } server.start end vpim-0.695/samples/mutt-aliases-to-vcf.rb0000755000076500000240000000171211152674120016753 0ustar samstaff#!/usr/bin/env ruby $-w = true $:.unshift File.dirname($0) + '/../lib' require 'vpim/vcard' ARGV.each do |file| File.open(file).each do |line| if line =~ /\s*alias\s+(\w+)\s+(.*)/ nick = $1 rhs = $2 email = nil name = nil case rhs when /(.*)<(.*)>/ email = $2 name = $1 else email = rhs name = nick nick = nil end card = Vpim::Vcard::Maker.make2 do |maker| # don't have the broken-down name, we'll have to leave it blank maker.name { |n| n.fullname = name } # Set preferred, its the only one... maker.add_email( email ) { |e| e.preferred = true } maker.nickname = nick if nick # mark as auto-generated, it makes it easier to see them maker.add_field( Vpim::DirectoryInfo::Field.create('note', "auto-generated-from-mutt-aliases") ) end puts card.to_s end end end vpim-0.695/samples/osx-wrappers.rb0000644000076500000240000000410711152674120015617 0ustar samstaff# OSX wrapper methods. # # The OSX classes are mirrored fairly directly into ruby by ruby/cocoa. Too # directly for ease, this is a start at convenient ruby APIs on top of the low-level cocoa # methods. =begin Ideas for things to add: + each for the addressbook + ABRecord#[] <- valueForProperty + [] and each for NSCFArray (which is actually an instance of OCObject) + [] and each for NSCFDictionary (which is actually an instance of OCObject) + Can I add methods to OCObject, and have them implement themselves based on the the 'class'? + ABMultiValue#[index] if index is a :token, then its the identifier, is a string, its a label is a number, its an array index return a Struct, so you can do mvalue["work"].value =end require 'osx/addressbook' # put into osx/ocobject? module OSX # When an NSData object is returned by an objective/c API (such as # ABPerson.vCardRepresentation, I actually get a OCObject back, who's class # instance points to either a NSData, or a related class. # # This is a convenience method to get the NSData data back as a ruby string. # Is it the right place to put this? class OCObject def bytes s = ' ' * length getBytes(s) s end end end # put into osx/abperson? module OSX class ABPerson def vCard card = self.vCardRepresentation.bytes # The card representation appears to be either ASCII, or UCS-2. If its # UCS-2, then the first byte will be 0, so check for this, and convert # if necessary. # # We know it's 0, because the first character in a vCard must be the 'B' # of "BEGIN:VCARD", and in UCS-2 all ascii are encoded as a 0 byte # followed by the ASCII byte, UNICODE is great. if card[0] == 0 nsstring = OSX::NSString.alloc.initWithCharacters(card, :length, card.size/2) card = nsstring.UTF8String # TODO: is nsstring.UTF8String == nsstring.to_s ? end card end end end # put into osx/group? module OSX class ABGroup def name self.valueForProperty(OSX::kABGroupNameProperty).to_s end end end vpim-0.695/samples/README.mutt0000644000076500000240000000524611152674120014475 0ustar samstaff ** cmd-itip.rb This script pretty-prints iTIP calendar invitations, often sent by email using iMIP as text/calendar objects. Download the latest vPim from: http://rubyforge.org/projects/vpim/ It requires Ruby to be installed. Install vpim: tar -xzf vpim-XX.tgz cd vpim-XX ruby install.rb config ruby install.rb setup sudo ruby install.rb install Install cmd-itip.rb into your path, perhaps without the extension. cp samples/cmd-itip.rb ~/bin/cmd-itip chmod +x ~/bin/cmd-itip Modify your ~/.mailcap or /etc/mailcap files to call cmd-itip, add a line like: text/calendar; cmd-itip --myaddr "sroberts@" %s; copiousoutput If you give a REGEX to --myaddr to tell cmd-itip your email addresses, cmd-itip will avoid printing some information on the attendees to an invitation. Modify muttrc to autoview calendars with a command like: auto_view text/calendar Notes on Notes; Because Domino sends a close-to-unreadable text/plain attachment along with the text/calendar in a multipart/alternative, and the text/plain is first in the alternatives, the garbage will be at the top, and the nicely printed calendar at the bottom. Because of this, I reorder the view preference so the calendar invitation is clearly printed at the top of the message with a muttrc command like: alternative_order text/calendar text/plain Domino also includes the calendar twice in the mail message, so you'll see it twice, I don't know what to do about that. Notes on application/octet-stream: Some calendar programs, such as Apple's Mail.app, wrongly send iCalendar attachments with a content-type of application/octet-stream. In order to be processed correctly, use the mutt 1.5 or later capability to lookup the correct MIME content-type based on the file extension. Put this in your muttrc file: mime_lookup application/octet-stream and ensure /etc/mime.types or ~/.mime.types contains: text/calendar ics ** vcf-to-mutt.rb This script searches a set of vCards can output the results as a Mutt query response, or a Mutt aliases file. It used to support querying the OS X Address Book, but that is better done with lbdb, see http://www.spinnaker.de/lbdb/. To install, you must: 1 - install vPim (see README) 3 - copy vcf-to-mutt into a directory in your path, such as ~/bin, and chmod +x vcf-to-mutt.rb to make it executable. 4 - Put in your muttrc file (either ~/.muttrc or ~/.mutt/muttrc) a line such as: set query_command = "vcf-to-mutt.rb '%s'" 5 - The query command ("Q") will query the address book, control-t will give you auto-completion of email addresses, see the Mutt manual page. ** mutt-aliases-to-vcf.rb This script converts a mutt aliases file into a vCard file. vpim-0.695/samples/reminder.rb0000755000076500000240000001015511152674120014755 0ustar samstaff#!/usr/bin/env ruby $-w = true require 'ubygems' rescue "ignored" require 'getoptlong' require 'pp' require 'plist' require 'vpim/repo' $stdout.sync = true $stderr.sync = true HELP =< 0 Vpim::Repo::Directory.each(ARGV.first) do |cal| calendars << cal end else Vpim::Repo::Apple3.new.each() do |cal| calendars << cal end end if opt_dump pp ARGV pp calendars end SECSPERDAY = (24 * 60 * 60) t0 = Time.new.to_a t0[0] = t0[1] = t0[2] = 0 # sec,min,hour = 0 t0 = Time.local(*t0) t1 = t0 + opt_days * SECSPERDAY if opt_dump puts "to: #{t0}" puts "t1: #{t1}" end if opt_verbose puts "Events in the next #{opt_days} days:" end # Collect all events, then all todos. all_events = [] all_todos = [] calendars.each do |cal| if opt_debug; puts "Calendar: #{cal.name}"; end # TODO - mv collection algorithm to library begin cal.events.each do |e| begin if opt_dump; pp e; end if e.occurs_in?(t0, t1) if e.summary all_events.push(e) end end rescue $stderr.puts "error in #{cal.name} (\"#{e.summary}\"): #{$!.to_s}" end end all_todos.concat(cal.todos.to_a) end end puts # TODO - mv sorting algorithm to library def start_of_first_occurrence(t0, t1, e) e.occurrences(t1) do |t| # An event might start before t0, but end after it..., in which case # we are still interested. if (t + (e.duration || 0)) >= t0 return t end end nil end all_events.sort! do |lhs, rhs| start_of_first_occurrence(t0, t1, lhs) <=> start_of_first_occurrence(t0, t1, rhs) end all_events.each do |e| puts "#{e.summary}:" if opt_verbose if e.description; puts " description=#{e.description}"; end if e.comments.any?; puts " comment=#{e.comments.first}"; end if e.location; puts " location=#{e.location}"; end if e.status; puts " status=#{e.status}"; end if e.dtstart; puts " dtstart=#{e.dtstart}"; end if e.duration; puts " duration=#{Vpim::Duration.new(e.duration).to_s}"; end end i = 1 e.occurrences(t1) do |t| # An event might start before t0, but end after it..., in which case # we are still interested. dstr = '' if e.duration dstr = " for #{Vpim::Duration.new(e.duration).to_s}" end # TODO - mv to library, as variant of occurs_in? if (t + (e.duration || 0)) >= t0 puts " ##{i} on #{t}#{dstr}" i += 1 end end end =begin def fix_priority(vtodo) p = vtodo.priority if !p p = 10 end =end all_todos.sort! do |x,y| x = x.priority y = y.priority # 0 means no priority, put these last, not first x = 10 if x == 0 y = 10 if y == 0 x <=> y end priorities = [ 'no importance', 'very important', 'very important', 'very important', 'important', 'important', 'important', 'not important', 'not important', 'not important' ] all_todos.each do |e| status = e.status || 'Todo' if status != 'COMPLETED' puts "#{status.capitalize}: #{e.summary}" # (#{priorities[e.priority]})" end end vpim-0.695/samples/rrule.rb0000755000076500000240000000253211152674120014301 0ustar samstaff#!/usr/bin/env ruby $-w = true $:.unshift File.dirname($0) + "/../lib" require 'vpim/rrule' require 'getoptlong' require 'parsedate' HELP =< vcards.vcf # # This command will create vcards and save them into vcards.vcf. # # License: Same as Ruby. # require 'vpim/vcard' # # Opens a file and attempts to parse out vcards. It is meant to work on a # tab-delimited file with column names in the first line, followed by # any number of records on the following lines. # class VCardParser # # Pass in a filename as a string, and get an array of VCard objects back. # def vcards_from_txt_file(filename) #puts "Parsing input file #{filename}" vcards = [] first_line = true VCardField.reset_custom_fields IO.foreach(filename) { |line| #puts "Parsing line: #{line}" if first_line == true @headers = headers_from_line(line) first_line = false else vcards << vcard_from_line(line) end } vcards end protected def headers_from_line(a_line) a_line.upcase.split("\t") end def fields_from_line(a_line) field_arr = headers_from_line(a_line) fields = {} @headers.each_index { |index| fields[@headers[index]]= field_arr[index] } fields end def vcard_from_line(a_line) #puts "vcard_from_line" # Parse the line from a tab-delimited text file. # The tricky part is that there may # be fields in the txt file which have commas between opening and closing # quotes, so don't just split on ','. # Get a hash of field names and values fields = fields_from_line(a_line) #puts "FirstName: " + fields["FIRST_NAME"] # 1. Look for the pattern /\".*,.*\"/ # 2. If found, save that pattern, and then substitute it with a # dynamic placeholder. # 3. Split the line on commas. # 4. For each item in the split, replace the substituted pattern # with the source pattern. #p fields # At this point, we should have an array of string values matching # the order of @headers. Construct a VCard using the header keys and # the parsed values vcard = Vpim::Vcard.create # Add the name field vcard << VCardField.create_n(fields["LAST_NAME"], fields["FIRST_NAME"], fields["MIDDLE_NAME"], fields["TITLE"], fields["SUFFIX"]) # Add the formal name display field vcard << VCardField.create_fn(fields["LAST_NAME"], fields["FIRST_NAME"], fields["MIDDLE_NAME"], fields["TITLE"], fields["SUFFIX"]) # Add Company & Department info vcard << VCardField.create_org(fields["COMPANY"], fields["DEPARTMENT"]) # Add Job Title info vcard << VCardField.create_job_title(fields["JOB_TITLE"]) # Add Phone Numbers vcard << VCardField.create_work_fax(fields["BUSINESS_FAX"]) vcard << VCardField.create_work_phone(fields["BUSINESS_PHONE"]) vcard << VCardField.create_work_phone(fields["BUSINESS_PHONE_2"]) vcard << VCardField.create_home_fax(fields["HOME_FAX"]) vcard << VCardField.create_home_phone(fields["HOME_PHONE"]) vcard << VCardField.create_home_phone(fields["HOME_PHONE_2"]) vcard << VCardField.create_cell_phone(fields["MOBILE_PHONE"]) vcard << VCardField.create_pager(fields["PAGER"]) vcard << VCardField.create_custom_phone(fields["OTHER_PHONE"], "other") # Add Business Address vcard << VCardField.create_business_address( fields["BUSINESS_STREET"], fields["BUSINESS_STREET_2"], fields["BUSINESS_STREET_3"], fields["BUSINESS_CITY"], fields["BUSINESS_STATE"], fields["BUSINESS_POSTAL_CODE"], fields["BUSINESS_COUNTRY"] ) # Add Home Address vcard << VCardField.create_home_address( fields["HOME_STREET"], fields["HOME_STREET_2"], fields["HOME_STREET_3"], fields["HOME_CITY"], fields["HOME_STATE"], fields["HOME_POSTAL_CODE"], fields["HOME_COUNTRY"] ) # Add Other Address vcard << VCardField.create_other_address( "Sample Other Address", fields["OTHER_STREET"], fields["OTHER_STREET_2"], fields["OTHER_STREET_3"], fields["OTHER_CITY"], fields["OTHER_STATE"], fields["OTHER_POSTAL_CODE"], fields["OTHER_COUNTRY"] ) # Add Emails vcard << VCardField.create_work_email(fields["E-MAIL_ADDRESS"]) vcard << VCardField.create_home_email(fields["E-MAIL_2_ADDRESS"]) vcard << VCardField.create_other_email(fields["E-MAIL_3_ADDRESS"], "other") # Add a note vcard << VCardField.create_note(fields["NOTES"]) vcard end end # # Subclass of Vpim::DirectoryInfo::Field adds a number of helpful methods for # creating VCard fields. # class VCardField < Vpim::DirectoryInfo::Field def VCardField.reset_custom_fields @@custom_number = 1 end # # Create a name field: "N" # def VCardField.create_n (last, first=nil, middle=nil, prefix=nil, suffix=nil) VCardField.create('N', "#{last};#{first};#{middle};#{prefix};#{suffix}") end protected def VCardField.valid_string(a_str) return a_str != nil && a_str.length > 0 end public # # Create a formal name field: "FN" # def VCardField.create_fn (last, first=nil, middle=nil, prefix=nil, suffix=nil) name = "" if valid_string(prefix) then name << "#{prefix} " end if valid_string(first) then name << "#{first} " end if valid_string(middle) then name << "#{middle} " end if valid_string(last) then name << "#{last} " end if valid_string(suffix) then name << "#{suffix} " end VCardField.create('FN', "#{name}") end # # Create a formal name field: "ORG" # def VCardField.create_org (organization_name, department_name=nil) VCardField.create("ORG", "#{organization_name};#{department_name}") end # # Create a title field: "TITLE" # def VCardField.create_job_title(title) VCardField.create("TITLE", title) end # # Create an email field: "EMAIL" with type="INTERNET" # # For _type_, use Ruby symbols :WORK or :HOME. # def VCardField.create_internet_email(address, type=:WORK, preferred_email=false) if preferred_email == true VCardField.create("EMAIL", address, "type" => ["INTERNET", type.to_s, "pref"]) else VCardField.create("EMAIL", address, "type" => ["INTERNET", type.to_s]) end end protected def VCardField.next_custom_name name = "item#{@@custom_number}" @@custom_number = @@custom_number + 1 name end def VCardField.create_phone(phone_num, is_preferred = false, type_arr = ["WORK"], custom_name = nil) field_name = "" if custom_name != nil field_name << next_custom_name field_name << "." end # Flatten the array so we can add additional items to it. type_arr = [type_arr].flatten # If this phone number is preferred, then add that into the type array. if is_preferred type_arr << "pref" end # Create the TEL field. ret_val = [VCardField.create("#{field_name}TEL", phone_num, "type" => type_arr)] # If we need a custom field . . . if custom_name != nil ret_val << VCardField.create("#{field_name}X-ABLabel", custom_name) end ret_val end public def VCardField.create_note(note_text) VCardField.create("NOTE", note_text) end def VCardField.create_custom_phone(phone_number, custom_name, is_preferred = false) VCardField.create_phone(phone_number, is_preferred, ["HOME"], custom_name) end def VCardField.create_pager(pager_number, is_preferred = false) VCardField.create_phone(pager_number, is_preferred, ["PAGER"]) end def VCardField.create_work_fax(fax_number, is_preferred = false) VCardField.create_phone(fax_number, is_preferred, ["FAX", "WORK"]) end def VCardField.create_work_phone(phone_number, is_preferred = false) VCardField.create_phone(phone_number, is_preferred, ["WORK"]) end def VCardField.create_home_fax(fax_number, is_preferred = false) VCardField.create_phone(fax_number, is_preferred, ["FAX", "HOME"]) end def VCardField.create_home_phone(phone_number, is_preferred = false) VCardField.create_phone(phone_number, is_preferred, ["HOME"]) end def VCardField.create_cell_phone(phone_number, is_preferred = false) VCardField.create_phone(phone_number, is_preferred, ["CELL"]) end def VCardField.create_other_address( address_label, street, street2 = "", street3 = "", city = "", state = "", postal_code = "", country = "", is_preferred = false ) VCardField.create_address(street, street2, street3, city, state, postal_code, country, is_preferred, ["HOME"], address_label) end def VCardField.create_home_address( street, street2 = "", street3 = "", city = "", state = "", postal_code = "", country = "", is_preferred = false ) VCardField.create_address(street, street2, street3, city, state, postal_code, country, is_preferred, ["HOME"]) end def VCardField.create_business_address( street, street2 = "", street3 = "", city = "", state = "", postal_code = "", country = "", is_preferred = false ) VCardField.create_address(street, street2, street3, city, state, postal_code, country, is_preferred, ["WORK"]) end def VCardField.create_work_email(address, is_preferred = false) VCardField.create_email(address, is_preferred, ["WORK"]) end def VCardField.create_home_email(address, is_preferred = false) VCardField.create_email(address, is_preferred, ["HOME"]) end def VCardField.create_other_email(address, custom_name, is_preferred = false) VCardField.create_email(address, is_preferred, ["WORK"], custom_name) end protected def VCardField.create_email(address, is_preferred, type_arr = ["WORK"], custom_name = nil) name = "" if custom_name != nil name << next_custom_name name << "." end if is_preferred type_arr << "pref" end ret_val = [VCardField.create("#{name}EMAIL", address, "type" => type_arr)] if custom_name != nil ret_val << VCardField.create("#{name}X-ABLabel", custom_name) end ret_val end def VCardField.create_address( street, street2 = "", street3 = "", city = "", state = "", postal_code = "", country = "", is_preferred = false, type_arr = ["WORK"], other_label = nil) # Addresses need custom names, so get the next custom name for this # VCard name = next_custom_name # Construct the address string by making an array of the fields, and # then joining them with ';' as the separator. address_str = [street, street2, street3, city, state, postal_code, country] # If this is preferred, add that type. if is_preferred type_arr << "pref" end # Return an array with two lines, one defining the address, the second # defining something else . . . is this the locale? Not sure, but this # is how Mac OS X 10.3.6 exports address fields -> VCards. fields = [ VCardField.create("#{name}.ADR", address_str.join(';'), "type" => type_arr), VCardField.create("#{name}.X-ABADR", "us"), ] if other_label != nil fields << VCardField.create("#{name}.X-ABLabel", "#{other_label}") end fields end end parser = VCardParser.new cards = parser.vcards_from_txt_file(ARGV[0]) #puts "" cards.each { |card| puts card.to_s } vpim-0.695/samples/vcf-dump.rb0000755000076500000240000000277511152674120014702 0ustar samstaff#!/usr/bin/env ruby $-w = true $:.unshift File.dirname($0) + '/../lib' require 'pp' require 'getoptlong' require 'vpim/vcard' HELP =<... Options -h,--help Print this helpful message. -n,--name Print the vCard name. -d,--debug Print debug information. Examples: EOF opt_name = nil opt_debug = nil opts = GetoptLong.new( [ "--help", "-h", GetoptLong::NO_ARGUMENT ], [ "--name", "-n", GetoptLong::NO_ARGUMENT ], [ "--debug", "-d", GetoptLong::NO_ARGUMENT ] ) opts.each do |opt, arg| case opt when "--help" then puts HELP exit 0 when "--name" then opt_name = true when "--debug" then opt_debug = true end end if ARGV.length < 1 puts "no vcard files specified, try -h!" exit 1 end ARGV.each do |file| cards = Vpim::Vcard.decode(open(file)) cards.each do |card| card.each do |field| puts "..#{field.name.capitalize}=#{field.value.inspect}" if field.group puts " group=#{field.group}" end field.each_param do |param, values| puts " #{param}=[#{values.join(", ")}]" end end if opt_name begin puts "#name=#{card.name.formatted}" rescue puts "! failed to decode name!" end end if opt_debug card.groups.sort.each do |group| card.enum_by_group(group).each do |field| puts "#{group} -> #{field.inspect}" end end end puts "" end end vpim-0.695/samples/vcf-lines.rb0000755000076500000240000000202111152674120015027 0ustar samstaff#!/usr/bin/env ruby $-w = true $:.unshift File.dirname($0) + '/../lib' require 'pp' require 'getoptlong' require 'vpim/vcard' HELP =<... Options -h,--help Print this helpful message. Examples: EOF opt_name = nil opt_debug = nil opts = GetoptLong.new( [ "--help", "-h", GetoptLong::NO_ARGUMENT ], [ "--name", "-n", GetoptLong::NO_ARGUMENT ], [ "--debug", "-d", GetoptLong::NO_ARGUMENT ] ) opts.each do |opt, arg| case opt when "--help" then puts HELP exit 0 when "--name" then opt_name = true when "--debug" then opt_debug = true end end if ARGV.length < 1 puts "no vcard files specified, try -h!" exit 1 end ARGV.each do |file| cards = Vpim::Vcard.decode(open(file)) cards.each do |card| card.lines.each_with_index do |line, i| print line.name if line.group.length > 0 print " (", line.group, ")" end print ": ", line.value.inspect, "\n" end end end vpim-0.695/samples/vcf-to-ics.rb0000755000076500000240000000074311152674120015124 0ustar samstaff#!/usr/bin/env ruby require 'vpim/vcard' require 'vpim/icalendar' $in = ARGV.first ? File.open(ARGV.shift) : $stdin $out = ARGV.first ? File.open(ARGV.shift, 'w') : $stdout cal = Vpim::Icalendar.create Vpim::Vcard.decode($in).each do |card| if card.birthday cal.push Vpim::Icalendar::Vevent.create_yearly( card.birthday, "Birthday for #{card['fn'].strip}" ) $stderr.puts "#{card['fn']} -> bday #{cal.events.last.dtstart}" end end puts cal.encode vpim-0.695/samples/vcf-to-mutt.rb0000755000076500000240000000551011152674120015334 0ustar samstaff#!/usr/bin/env ruby # # For a query command, mutt expects output of the form: # # informational line # TAB[TAB] # ... # # For an alias command, mutt expects output of the form: # alias NICKNAME EMAIL # # NICKNAME shouldn't have spaces, and EMAIL can be either "user@example.com", # "", or "User ". $-w = true $:.unshift File.dirname($0) + '/../lib' require 'getoptlong' require 'vpim/vcard' HELP =<" end end end end cards = Vpim::Vcard.decode($stdin) matches = Mutt::vcard_query(cards, opt_query) if opt_aliases Mutt::alias_print(matches) else qstr = opt_query == '' ? '' : opt_query; Mutt::query_print(matches, "Query #{qstr} against #{cards.size} vCards:") end vpim-0.695/setup.rb0000644000076500000240000010650211152674120012643 0ustar samstaff# # setup.rb # # Copyright (c) 2000-2005 Minero Aoki # # This program is free software. # You can distribute/modify this program under the terms of # the GNU LGPL, Lesser General Public License version 2.1. # unless Enumerable.method_defined?(:map) # Ruby 1.4.6 module Enumerable alias map collect end end unless File.respond_to?(:read) # Ruby 1.6 def File.read(fname) open(fname) {|f| return f.read } end end unless Errno.const_defined?(:ENOTEMPTY) # Windows? module Errno class ENOTEMPTY # We do not raise this exception, implementation is not needed. end end end def File.binread(fname) open(fname, 'rb') {|f| return f.read } end # for corrupted Windows' stat(2) def File.dir?(path) File.directory?((path[-1,1] == '/') ? path : path + '/') end class ConfigTable include Enumerable def initialize(rbconfig) @rbconfig = rbconfig @items = [] @table = {} # options @install_prefix = nil @config_opt = nil @verbose = true @no_harm = false end attr_accessor :install_prefix attr_accessor :config_opt attr_writer :verbose def verbose? @verbose end attr_writer :no_harm def no_harm? @no_harm end def [](key) lookup(key).resolve(self) end def []=(key, val) lookup(key).set val end def names @items.map {|i| i.name } end def each(&block) @items.each(&block) end def key?(name) @table.key?(name) end def lookup(name) @table[name] or setup_rb_error "no such config item: #{name}" end def add(item) @items.push item @table[item.name] = item end def remove(name) item = lookup(name) @items.delete_if {|i| i.name == name } @table.delete_if {|name, i| i.name == name } item end def load_script(path, inst = nil) if File.file?(path) MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path end end def savefile '.config' end def load_savefile begin File.foreach(savefile()) do |line| k, v = *line.split(/=/, 2) self[k] = v.strip end rescue Errno::ENOENT setup_rb_error $!.message + "\n#{File.basename($0)} config first" end end def save @items.each {|i| i.value } File.open(savefile(), 'w') {|f| @items.each do |i| f.printf "%s=%s\n", i.name, i.value if i.value? and i.value end } end def load_standard_entries standard_entries(@rbconfig).each do |ent| add ent end end def standard_entries(rbconfig) c = rbconfig rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT']) major = c['MAJOR'].to_i minor = c['MINOR'].to_i teeny = c['TEENY'].to_i version = "#{major}.#{minor}" # ruby ver. >= 1.4.4? newpath_p = ((major >= 2) or ((major == 1) and ((minor >= 5) or ((minor == 4) and (teeny >= 4))))) if c['rubylibdir'] # V > 1.6.3 libruby = "#{c['prefix']}/lib/ruby" librubyver = c['rubylibdir'] librubyverarch = c['archdir'] siteruby = c['sitedir'] siterubyver = c['sitelibdir'] siterubyverarch = c['sitearchdir'] elsif newpath_p # 1.4.4 <= V <= 1.6.3 libruby = "#{c['prefix']}/lib/ruby" librubyver = "#{c['prefix']}/lib/ruby/#{version}" librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" siteruby = c['sitedir'] siterubyver = "$siteruby/#{version}" siterubyverarch = "$siterubyver/#{c['arch']}" else # V < 1.4.4 libruby = "#{c['prefix']}/lib/ruby" librubyver = "#{c['prefix']}/lib/ruby/#{version}" librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" siteruby = "#{c['prefix']}/lib/ruby/#{version}/site_ruby" siterubyver = siteruby siterubyverarch = "$siterubyver/#{c['arch']}" end parameterize = lambda {|path| path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix') } if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } makeprog = arg.sub(/'/, '').split(/=/, 2)[1] else makeprog = 'make' end [ ExecItem.new('installdirs', 'std/site/home', 'std: install under libruby; site: install under site_ruby; home: install under $HOME')\ {|val, table| case val when 'std' table['rbdir'] = '$librubyver' table['sodir'] = '$librubyverarch' when 'site' table['rbdir'] = '$siterubyver' table['sodir'] = '$siterubyverarch' when 'home' setup_rb_error '$HOME was not set' unless ENV['HOME'] table['prefix'] = ENV['HOME'] table['rbdir'] = '$libdir/ruby' table['sodir'] = '$libdir/ruby' end }, PathItem.new('prefix', 'path', c['prefix'], 'path prefix of target environment'), PathItem.new('bindir', 'path', parameterize.call(c['bindir']), 'the directory for commands'), PathItem.new('libdir', 'path', parameterize.call(c['libdir']), 'the directory for libraries'), PathItem.new('datadir', 'path', parameterize.call(c['datadir']), 'the directory for shared data'), PathItem.new('mandir', 'path', parameterize.call(c['mandir']), 'the directory for man pages'), PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']), 'the directory for system configuration files'), PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']), 'the directory for local state data'), PathItem.new('libruby', 'path', libruby, 'the directory for ruby libraries'), PathItem.new('librubyver', 'path', librubyver, 'the directory for standard ruby libraries'), PathItem.new('librubyverarch', 'path', librubyverarch, 'the directory for standard ruby extensions'), PathItem.new('siteruby', 'path', siteruby, 'the directory for version-independent aux ruby libraries'), PathItem.new('siterubyver', 'path', siterubyver, 'the directory for aux ruby libraries'), PathItem.new('siterubyverarch', 'path', siterubyverarch, 'the directory for aux ruby binaries'), PathItem.new('rbdir', 'path', '$siterubyver', 'the directory for ruby scripts'), PathItem.new('sodir', 'path', '$siterubyverarch', 'the directory for ruby extentions'), PathItem.new('rubypath', 'path', rubypath, 'the path to set to #! line'), ProgramItem.new('rubyprog', 'name', rubypath, 'the ruby program using for installation'), ProgramItem.new('makeprog', 'name', makeprog, 'the make program to compile ruby extentions'), SelectItem.new('shebang', 'all/ruby/never', 'ruby', 'shebang line (#!) editing mode'), BoolItem.new('without-ext', 'yes/no', 'no', 'does not compile/install ruby extentions') ] end private :standard_entries def load_multipackage_entries multipackage_entries().each do |ent| add ent end end def multipackage_entries [ PackageSelectionItem.new('with', 'name,name...', '', 'ALL', 'package names that you want to install'), PackageSelectionItem.new('without', 'name,name...', '', 'NONE', 'package names that you do not want to install') ] end private :multipackage_entries ALIASES = { 'std-ruby' => 'librubyver', 'stdruby' => 'librubyver', 'rubylibdir' => 'librubyver', 'archdir' => 'librubyverarch', 'site-ruby-common' => 'siteruby', # For backward compatibility 'site-ruby' => 'siterubyver', # For backward compatibility 'bin-dir' => 'bindir', 'bin-dir' => 'bindir', 'rb-dir' => 'rbdir', 'so-dir' => 'sodir', 'data-dir' => 'datadir', 'ruby-path' => 'rubypath', 'ruby-prog' => 'rubyprog', 'ruby' => 'rubyprog', 'make-prog' => 'makeprog', 'make' => 'makeprog' } def fixup ALIASES.each do |ali, name| @table[ali] = @table[name] end @items.freeze @table.freeze @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/ end def parse_opt(opt) m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}" m.to_a[1,2] end def dllext @rbconfig['DLEXT'] end def value_config?(name) lookup(name).value? end class Item def initialize(name, template, default, desc) @name = name.freeze @template = template @value = default @default = default @description = desc end attr_reader :name attr_reader :description attr_accessor :default alias help_default default def help_opt "--#{@name}=#{@template}" end def value? true end def value @value end def resolve(table) @value.gsub(%r<\$([^/]+)>) { table[$1] } end def set(val) @value = check(val) end private def check(val) setup_rb_error "config: --#{name} requires argument" unless val val end end class BoolItem < Item def config_type 'bool' end def help_opt "--#{@name}" end private def check(val) return 'yes' unless val case val when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes' when /\An(o)?\z/i, /\Af(alse)\z/i then 'no' else setup_rb_error "config: --#{@name} accepts only yes/no for argument" end end end class PathItem < Item def config_type 'path' end private def check(path) setup_rb_error "config: --#{@name} requires argument" unless path path[0,1] == '$' ? path : File.expand_path(path) end end class ProgramItem < Item def config_type 'program' end end class SelectItem < Item def initialize(name, selection, default, desc) super @ok = selection.split('/') end def config_type 'select' end private def check(val) unless @ok.include?(val.strip) setup_rb_error "config: use --#{@name}=#{@template} (#{val})" end val.strip end end class ExecItem < Item def initialize(name, selection, desc, &block) super name, selection, nil, desc @ok = selection.split('/') @action = block end def config_type 'exec' end def value? false end def resolve(table) setup_rb_error "$#{name()} wrongly used as option value" end undef set def evaluate(val, table) v = val.strip.downcase unless @ok.include?(v) setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})" end @action.call v, table end end class PackageSelectionItem < Item def initialize(name, template, default, help_default, desc) super name, template, default, desc @help_default = help_default end attr_reader :help_default def config_type 'package' end private def check(val) unless File.dir?("packages/#{val}") setup_rb_error "config: no such package: #{val}" end val end end class MetaConfigEnvironment def initialize(config, installer) @config = config @installer = installer end def config_names @config.names end def config?(name) @config.key?(name) end def bool_config?(name) @config.lookup(name).config_type == 'bool' end def path_config?(name) @config.lookup(name).config_type == 'path' end def value_config?(name) @config.lookup(name).config_type != 'exec' end def add_config(item) @config.add item end def add_bool_config(name, default, desc) @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) end def add_path_config(name, default, desc) @config.add PathItem.new(name, 'path', default, desc) end def set_config_default(name, default) @config.lookup(name).default = default end def remove_config(name) @config.remove(name) end # For only multipackage def packages raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer @installer.packages end # For only multipackage def declare_packages(list) raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer @installer.packages = list end end end # class ConfigTable # This module requires: #verbose?, #no_harm? module FileOperations def mkdir_p(dirname, prefix = nil) dirname = prefix + File.expand_path(dirname) if prefix $stderr.puts "mkdir -p #{dirname}" if verbose? return if no_harm? # Does not check '/', it's too abnormal. dirs = File.expand_path(dirname).split(%r<(?=/)>) if /\A[a-z]:\z/i =~ dirs[0] disk = dirs.shift dirs[0] = disk + dirs[0] end dirs.each_index do |idx| path = dirs[0..idx].join('') Dir.mkdir path unless File.dir?(path) end end def rm_f(path) $stderr.puts "rm -f #{path}" if verbose? return if no_harm? force_remove_file path end def rm_rf(path) $stderr.puts "rm -rf #{path}" if verbose? return if no_harm? remove_tree path end def remove_tree(path) if File.symlink?(path) remove_file path elsif File.dir?(path) remove_tree0 path else force_remove_file path end end def remove_tree0(path) Dir.foreach(path) do |ent| next if ent == '.' next if ent == '..' entpath = "#{path}/#{ent}" if File.symlink?(entpath) remove_file entpath elsif File.dir?(entpath) remove_tree0 entpath else force_remove_file entpath end end begin Dir.rmdir path rescue Errno::ENOTEMPTY # directory may not be empty end end def move_file(src, dest) force_remove_file dest begin File.rename src, dest rescue File.open(dest, 'wb') {|f| f.write File.binread(src) } File.chmod File.stat(src).mode, dest File.unlink src end end def force_remove_file(path) begin remove_file path rescue end end def remove_file(path) File.chmod 0777, path File.unlink path end def install(from, dest, mode, prefix = nil) $stderr.puts "install #{from} #{dest}" if verbose? return if no_harm? realdest = prefix ? prefix + File.expand_path(dest) : dest realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) str = File.binread(from) if diff?(str, realdest) verbose_off { rm_f realdest if File.exist?(realdest) } File.open(realdest, 'wb') {|f| f.write str } File.chmod mode, realdest File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| if prefix f.puts realdest.sub(prefix, '') else f.puts realdest end } end end def diff?(new_content, path) return true unless File.exist?(path) new_content != File.binread(path) end def command(*args) $stderr.puts args.join(' ') if verbose? system(*args) or raise RuntimeError, "system(#{args.map{|a| a.inspect }.join(' ')}) failed" end def ruby(*args) command config('rubyprog'), *args end def make(task = nil) command(*[config('makeprog'), task].compact) end def extdir?(dir) File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb") end def files_of(dir) Dir.open(dir) {|d| return d.select {|ent| File.file?("#{dir}/#{ent}") } } end DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn ) def directories_of(dir) Dir.open(dir) {|d| return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT } end end # This module requires: #srcdir_root, #objdir_root, #relpath module HookScriptAPI def get_config(key) @config[key] end alias config get_config # obsolete: use metaconfig to change configuration def set_config(key, val) @config[key] = val end # # srcdir/objdir (works only in the package directory) # def curr_srcdir "#{srcdir_root()}/#{relpath()}" end def curr_objdir "#{objdir_root()}/#{relpath()}" end def srcfile(path) "#{curr_srcdir()}/#{path}" end def srcexist?(path) File.exist?(srcfile(path)) end def srcdirectory?(path) File.dir?(srcfile(path)) end def srcfile?(path) File.file?(srcfile(path)) end def srcentries(path = '.') Dir.open("#{curr_srcdir()}/#{path}") {|d| return d.to_a - %w(. ..) } end def srcfiles(path = '.') srcentries(path).select {|fname| File.file?(File.join(curr_srcdir(), path, fname)) } end def srcdirectories(path = '.') srcentries(path).select {|fname| File.dir?(File.join(curr_srcdir(), path, fname)) } end end class ToplevelInstaller Version = '3.4.1' Copyright = 'Copyright (c) 2000-2005 Minero Aoki' TASKS = [ [ 'all', 'do config, setup, then install' ], [ 'config', 'saves your configurations' ], [ 'show', 'shows current configuration' ], [ 'setup', 'compiles ruby extentions and others' ], [ 'install', 'installs files' ], [ 'test', 'run all tests in test/' ], [ 'clean', "does `make clean' for each extention" ], [ 'distclean',"does `make distclean' for each extention" ] ] def ToplevelInstaller.invoke config = ConfigTable.new(load_rbconfig()) config.load_standard_entries config.load_multipackage_entries if multipackage? config.fixup klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller) klass.new(File.dirname($0), config).invoke end def ToplevelInstaller.multipackage? File.dir?(File.dirname($0) + '/packages') end def ToplevelInstaller.load_rbconfig if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } ARGV.delete(arg) load File.expand_path(arg.split(/=/, 2)[1]) $".push 'rbconfig.rb' else require 'rbconfig' end ::Config::CONFIG end def initialize(ardir_root, config) @ardir = File.expand_path(ardir_root) @config = config # cache @valid_task_re = nil end def config(key) @config[key] end def inspect "#<#{self.class} #{__id__()}>" end def invoke run_metaconfigs case task = parsearg_global() when nil, 'all' parsearg_config init_installers exec_config exec_setup exec_install else case task when 'config', 'test' ; when 'clean', 'distclean' @config.load_savefile if File.exist?(@config.savefile) else @config.load_savefile end __send__ "parsearg_#{task}" init_installers __send__ "exec_#{task}" end end def run_metaconfigs @config.load_script "#{@ardir}/metaconfig" end def init_installers @installer = Installer.new(@config, @ardir, File.expand_path('.')) end # # Hook Script API bases # def srcdir_root @ardir end def objdir_root '.' end def relpath '.' end # # Option Parsing # def parsearg_global while arg = ARGV.shift case arg when /\A\w+\z/ setup_rb_error "invalid task: #{arg}" unless valid_task?(arg) return arg when '-q', '--quiet' @config.verbose = false when '--verbose' @config.verbose = true when '--help' print_usage $stdout exit 0 when '--version' puts "#{File.basename($0)} version #{Version}" exit 0 when '--copyright' puts Copyright exit 0 else setup_rb_error "unknown global option '#{arg}'" end end nil end def valid_task?(t) valid_task_re() =~ t end def valid_task_re @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/ end def parsearg_no_options unless ARGV.empty? task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1) setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}" end end alias parsearg_show parsearg_no_options alias parsearg_setup parsearg_no_options alias parsearg_test parsearg_no_options alias parsearg_clean parsearg_no_options alias parsearg_distclean parsearg_no_options def parsearg_config evalopt = [] set = [] @config.config_opt = [] while i = ARGV.shift if /\A--?\z/ =~ i @config.config_opt = ARGV.dup break end name, value = *@config.parse_opt(i) if @config.value_config?(name) @config[name] = value else evalopt.push [name, value] end set.push name end evalopt.each do |name, value| @config.lookup(name).evaluate value, @config end # Check if configuration is valid set.each do |n| @config[n] if @config.value_config?(n) end end def parsearg_install @config.no_harm = false @config.install_prefix = '' while a = ARGV.shift case a when '--no-harm' @config.no_harm = true when /\A--prefix=/ path = a.split(/=/, 2)[1] path = File.expand_path(path) unless path[0,1] == '/' @config.install_prefix = path else setup_rb_error "install: unknown option #{a}" end end end def print_usage(out) out.puts 'Typical Installation Procedure:' out.puts " $ ruby #{File.basename $0} config" out.puts " $ ruby #{File.basename $0} setup" out.puts " # ruby #{File.basename $0} install (may require root privilege)" out.puts out.puts 'Detailed Usage:' out.puts " ruby #{File.basename $0} " out.puts " ruby #{File.basename $0} [] []" fmt = " %-24s %s\n" out.puts out.puts 'Global options:' out.printf fmt, '-q,--quiet', 'suppress message outputs' out.printf fmt, ' --verbose', 'output messages verbosely' out.printf fmt, ' --help', 'print this message' out.printf fmt, ' --version', 'print version and quit' out.printf fmt, ' --copyright', 'print copyright and quit' out.puts out.puts 'Tasks:' TASKS.each do |name, desc| out.printf fmt, name, desc end fmt = " %-24s %s [%s]\n" out.puts out.puts 'Options for CONFIG or ALL:' @config.each do |item| out.printf fmt, item.help_opt, item.description, item.help_default end out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" out.puts out.puts 'Options for INSTALL:' out.printf fmt, '--no-harm', 'only display what to do if given', 'off' out.printf fmt, '--prefix=path', 'install path prefix', '' out.puts end # # Task Handlers # def exec_config @installer.exec_config @config.save # must be final end def exec_setup @installer.exec_setup end def exec_install @installer.exec_install end def exec_test @installer.exec_test end def exec_show @config.each do |i| printf "%-20s %s\n", i.name, i.value if i.value? end end def exec_clean @installer.exec_clean end def exec_distclean @installer.exec_distclean end end # class ToplevelInstaller class ToplevelInstallerMulti < ToplevelInstaller include FileOperations def initialize(ardir_root, config) super @packages = directories_of("#{@ardir}/packages") raise 'no package exists' if @packages.empty? @root_installer = Installer.new(@config, @ardir, File.expand_path('.')) end def run_metaconfigs @config.load_script "#{@ardir}/metaconfig", self @packages.each do |name| @config.load_script "#{@ardir}/packages/#{name}/metaconfig" end end attr_reader :packages def packages=(list) raise 'package list is empty' if list.empty? list.each do |name| raise "directory packages/#{name} does not exist"\ unless File.dir?("#{@ardir}/packages/#{name}") end @packages = list end def init_installers @installers = {} @packages.each do |pack| @installers[pack] = Installer.new(@config, "#{@ardir}/packages/#{pack}", "packages/#{pack}") end with = extract_selection(config('with')) without = extract_selection(config('without')) @selected = @installers.keys.select {|name| (with.empty? or with.include?(name)) \ and not without.include?(name) } end def extract_selection(list) a = list.split(/,/) a.each do |name| setup_rb_error "no such package: #{name}" unless @installers.key?(name) end a end def print_usage(f) super f.puts 'Inluded packages:' f.puts ' ' + @packages.sort.join(' ') f.puts end # # Task Handlers # def exec_config run_hook 'pre-config' each_selected_installers {|inst| inst.exec_config } run_hook 'post-config' @config.save # must be final end def exec_setup run_hook 'pre-setup' each_selected_installers {|inst| inst.exec_setup } run_hook 'post-setup' end def exec_install run_hook 'pre-install' each_selected_installers {|inst| inst.exec_install } run_hook 'post-install' end def exec_test run_hook 'pre-test' each_selected_installers {|inst| inst.exec_test } run_hook 'post-test' end def exec_clean rm_f @config.savefile run_hook 'pre-clean' each_selected_installers {|inst| inst.exec_clean } run_hook 'post-clean' end def exec_distclean rm_f @config.savefile run_hook 'pre-distclean' each_selected_installers {|inst| inst.exec_distclean } run_hook 'post-distclean' end # # lib # def each_selected_installers Dir.mkdir 'packages' unless File.dir?('packages') @selected.each do |pack| $stderr.puts "Processing the package `#{pack}' ..." if verbose? Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") Dir.chdir "packages/#{pack}" yield @installers[pack] Dir.chdir '../..' end end def run_hook(id) @root_installer.run_hook id end # module FileOperations requires this def verbose? @config.verbose? end # module FileOperations requires this def no_harm? @config.no_harm? end end # class ToplevelInstallerMulti class Installer FILETYPES = %w( bin lib ext data conf man ) include FileOperations include HookScriptAPI def initialize(config, srcroot, objroot) @config = config @srcdir = File.expand_path(srcroot) @objdir = File.expand_path(objroot) @currdir = '.' end def inspect "#<#{self.class} #{File.basename(@srcdir)}>" end def noop(rel) end # # Hook Script API base methods # def srcdir_root @srcdir end def objdir_root @objdir end def relpath @currdir end # # Config Access # # module FileOperations requires this def verbose? @config.verbose? end # module FileOperations requires this def no_harm? @config.no_harm? end def verbose_off begin save, @config.verbose = @config.verbose?, false yield ensure @config.verbose = save end end # # TASK config # def exec_config exec_task_traverse 'config' end alias config_dir_bin noop alias config_dir_lib noop def config_dir_ext(rel) extconf if extdir?(curr_srcdir()) end alias config_dir_data noop alias config_dir_conf noop alias config_dir_man noop def extconf ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt end # # TASK setup # def exec_setup exec_task_traverse 'setup' end def setup_dir_bin(rel) files_of(curr_srcdir()).each do |fname| update_shebang_line "#{curr_srcdir()}/#{fname}" end end alias setup_dir_lib noop def setup_dir_ext(rel) make if extdir?(curr_srcdir()) end alias setup_dir_data noop alias setup_dir_conf noop alias setup_dir_man noop def update_shebang_line(path) return if no_harm? return if config('shebang') == 'never' old = Shebang.load(path) if old $stderr.puts "warning: #{path}: Shebang line includes too many args. It is not portable and your program may not work." if old.args.size > 1 new = new_shebang(old) return if new.to_s == old.to_s else return unless config('shebang') == 'all' new = Shebang.new(config('rubypath')) end $stderr.puts "updating shebang: #{File.basename(path)}" if verbose? open_atomic_writer(path) {|output| File.open(path, 'rb') {|f| f.gets if old # discard output.puts new.to_s output.print f.read } } end def new_shebang(old) if /\Aruby/ =~ File.basename(old.cmd) Shebang.new(config('rubypath'), old.args) elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby' Shebang.new(config('rubypath'), old.args[1..-1]) else return old unless config('shebang') == 'all' Shebang.new(config('rubypath')) end end def open_atomic_writer(path, &block) tmpfile = File.basename(path) + '.tmp' begin File.open(tmpfile, 'wb', &block) File.rename tmpfile, File.basename(path) ensure File.unlink tmpfile if File.exist?(tmpfile) end end class Shebang def Shebang.load(path) line = nil File.open(path) {|f| line = f.gets } return nil unless /\A#!/ =~ line parse(line) end def Shebang.parse(line) cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ') new(cmd, args) end def initialize(cmd, args = []) @cmd = cmd @args = args end attr_reader :cmd attr_reader :args def to_s "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}") end end # # TASK install # def exec_install rm_f 'InstalledFiles' exec_task_traverse 'install' end def install_dir_bin(rel) install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755 end def install_dir_lib(rel) install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644 end def install_dir_ext(rel) return unless extdir?(curr_srcdir()) install_files rubyextentions('.'), "#{config('sodir')}/#{File.dirname(rel)}", 0555 end def install_dir_data(rel) install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644 end def install_dir_conf(rel) # FIXME: should not remove current config files # (rename previous file to .old/.org) install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644 end def install_dir_man(rel) install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644 end def install_files(list, dest, mode) mkdir_p dest, @config.install_prefix list.each do |fname| install fname, dest, mode, @config.install_prefix end end def libfiles glob_reject(%w(*.y *.output), targetfiles()) end def rubyextentions(dir) ents = glob_select("*.#{@config.dllext}", targetfiles()) if ents.empty? setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" end ents end def targetfiles mapdir(existfiles() - hookfiles()) end def mapdir(ents) ents.map {|ent| if File.exist?(ent) then ent # objdir else "#{curr_srcdir()}/#{ent}" # srcdir end } end # picked up many entries from cvs-1.11.1/src/ignore.c JUNK_FILES = %w( core RCSLOG tags TAGS .make.state .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb *~ *.old *.bak *.BAK *.orig *.rej _$* *$ *.org *.in .* ) def existfiles glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.'))) end def hookfiles %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| %w( config setup install clean ).map {|t| sprintf(fmt, t) } }.flatten end def glob_select(pat, ents) re = globs2re([pat]) ents.select {|ent| re =~ ent } end def glob_reject(pats, ents) re = globs2re(pats) ents.reject {|ent| re =~ ent } end GLOB2REGEX = { '.' => '\.', '$' => '\$', '#' => '\#', '*' => '.*' } def globs2re(pats) /\A(?:#{ pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|') })\z/ end # # TASK test # TESTDIR = 'test' def exec_test unless File.directory?('test') $stderr.puts 'no test in this package' if verbose? return end $stderr.puts 'Running tests...' if verbose? begin require 'test/unit' rescue LoadError setup_rb_error 'test/unit cannot loaded. You need Ruby 1.8 or later to invoke this task.' end runner = Test::Unit::AutoRunner.new(true) runner.to_run << TESTDIR runner.run end # # TASK clean # def exec_clean exec_task_traverse 'clean' rm_f @config.savefile rm_f 'InstalledFiles' end alias clean_dir_bin noop alias clean_dir_lib noop alias clean_dir_data noop alias clean_dir_conf noop alias clean_dir_man noop def clean_dir_ext(rel) return unless extdir?(curr_srcdir()) make 'clean' if File.file?('Makefile') end # # TASK distclean # def exec_distclean exec_task_traverse 'distclean' rm_f @config.savefile rm_f 'InstalledFiles' end alias distclean_dir_bin noop alias distclean_dir_lib noop def distclean_dir_ext(rel) return unless extdir?(curr_srcdir()) make 'distclean' if File.file?('Makefile') end alias distclean_dir_data noop alias distclean_dir_conf noop alias distclean_dir_man noop # # Traversing # def exec_task_traverse(task) run_hook "pre-#{task}" FILETYPES.each do |type| if type == 'ext' and config('without-ext') == 'yes' $stderr.puts 'skipping ext/* by user option' if verbose? next end traverse task, type, "#{task}_dir_#{type}" end run_hook "post-#{task}" end def traverse(task, rel, mid) dive_into(rel) { run_hook "pre-#{task}" __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') directories_of(curr_srcdir()).each do |d| traverse task, "#{rel}/#{d}", mid end run_hook "post-#{task}" } end def dive_into(rel) return unless File.dir?("#{@srcdir}/#{rel}") dir = File.basename(rel) Dir.mkdir dir unless File.dir?(dir) prevdir = Dir.pwd Dir.chdir dir $stderr.puts '---> ' + rel if verbose? @currdir = rel yield Dir.chdir prevdir $stderr.puts '<--- ' + rel if verbose? @currdir = File.dirname(rel) end def run_hook(id) path = [ "#{curr_srcdir()}/#{id}", "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) } return unless path begin instance_eval File.read(path), path, 1 rescue raise if $DEBUG setup_rb_error "hook #{path} failed:\n" + $!.message end end end # class Installer class SetupError < StandardError; end def setup_rb_error(msg) raise SetupError, msg end if $0 == __FILE__ begin ToplevelInstaller.invoke rescue SetupError raise if $DEBUG $stderr.puts $!.message $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." exit 1 end end vpim-0.695/test/0000755000076500000240000000000011152674120012131 5ustar samstaffvpim-0.695/test/test_agent_app.rb0000644000076500000240000000333011152674120015452 0ustar samstaffrequire 'test/common' require 'sinatra/test/unit' require 'vpim/agent/app' class IcsAgent < Test::Unit::TestCase def to_str @caldata end def setup @thrd = data_on_port(self, 9876) end def teardown @thrd.kill end def test_ics_atom_query @caldata = open('test/calendars/weather.calendar/Events/1205042405-0-0.ics').read get '/ics/atom?http://127.0.0.1:9876' #pp @response assert(@response.body =~ /<\?xml/) assert_equal(Vpim::Agent::Atomize::MIME, @response['Content-Type']) assert_equal(200, @response.status) assert(@response.body =~ Regexp.new( Regexp.quote( "http://example.org/ics/atom?http://127.0.0.1:9876" )), @response.body) end def test_ics get '/ics' assert(@response.body =~ /Subscribe")), @response.body) end def test_ics_query @caldata = open('test/calendars/weather.calendar/Events/1205042405-0-0.ics').read get '/ics?http://127.0.0.1:9876' assert(@response.body =~ / => <#{dec}>") assert_equal("+\\+\n+\n+,+;+a+b+c+", dec) enc_out = Vpim.encode_text(dec) should_be = "+\\\\+\\n+\\n+\\,+\\;+a+b+c+" # Note a, b, and c are allowed to be escaped, but shouldn't be and # aren't in output #puts("<#{dec}> => <#{enc_out}>") assert_equal(should_be, enc_out) end def test_field4 line = 't;e=a,b: 4 ' part = Field.decode0(line) assert_equal("4", part[ 3 ]) end def test_field3 line = 't;e=a,b:4' part = Field.decode0(line) assert_equal("4", part[ 3 ]) assert_equal( {'E' => [ 'a','b' ] }, part[ 2 ]) end def test_field2 line = 'tel;type=work,voice,msg:+1 313 747-4454' part = Field.decode0(line) assert_equal("+1 313 747-4454", part[ 3 ]) assert_equal( {'TYPE' => [ 'work','voice','msg' ] }, part[ 2 ]) end def test_field1 line = 'ORGANIZER;CN="xxxx, xxxx [SC100:370:EXCH]":MAILTO:xxxx@americasm01.nt.com' parts = Field.decode0(line) assert_equal(nil, parts[0]) assert_equal('ORGANIZER', parts[1]) assert_equal({ 'CN' => [ "xxxx, xxxx [SC100:370:EXCH]" ] }, parts[2]) assert_equal('MAILTO:xxxx@americasm01.nt.com', parts[3]) end =begin this can not be done :-( def test_case_equiv line = 'ORGANIZER;CN="xxxx, xxxx [SC100:370:EXCH]":MAILTO:xxxx@americasm01.nt.com' field = Field.decode(line) assert_equal(true, field.name?('organIZER')) assert_equal(true, field === 'organIZER') b = nil case field when 'organIZER' b = true end assert_equal(true, b) end =end def test_field0 assert_equal('name:', line = Field.encode0(nil, 'name')) assert_equal([ nil, 'NAME', {}, ''], Field.decode0(line)) assert_equal('name:value', line = Field.encode0(nil, 'name', {}, 'value')) assert_equal([ nil, 'NAME', {}, 'value'], Field.decode0(line)) assert_equal('name;encoding=B:dmFsdWU=', line = Field.encode0(nil, 'name', { 'encoding'=>:b64 }, 'value')) assert_equal([ nil, 'NAME', { 'ENCODING'=>['B']}, ['value'].pack('m').chomp ], Field.decode0(line)) assert_equal('group.name:value', line = Field.encode0('group', 'name', {}, 'value')) assert_equal([ 'GROUP', 'NAME', {}, 'value'], Field.decode0(line)) end def tEst_invalid_fields [ 'g.:', ':v', ].each do |line| assert_raises(Vpim::InvalidEncodingError) { Field.decode0(line) } end end def test_date_encode assert_equal("DTSTART:20040101\n", Field.create('DTSTART', Date.new(2004, 1, 1) ).to_s) assert_equal("DTSTART:20040101\n", Field.create('DTSTART', [Date.new(2004, 1, 1)]).to_s) end def test_field_modify f = Field.create('name') assert_equal('', f.value) f.value = '' assert_equal('', f.value) f.value = 'z' assert_equal('z', f.value) f.group = 'z.b' assert_equal('Z.B', f.group) assert_equal("z.b.NAME:z\n", f.encode) assert_raises(TypeError) { f.value = :group } assert_equal('Z.B', f.group) assert_equal("z.b.NAME:z\n", f.encode) assert_raises(TypeError) { f.group = :group } assert_equal("z.b.NAME:z\n", f.encode) assert_equal('Z.B', f.group) f['p0'] = "hi julie" assert_equal("Z.B.NAME;P0=hi julie:z\n", f.encode) assert_equal(['hi julie'], f.param('p0')) assert_equal(['hi julie'], f['p0']) assert_equal('NAME', f.name) assert_equal('Z.B', f.group) # FAIL assert_raises(ArgumentError) { f.group = 'z.b:' } assert_equal('Z.B', f.group) f.value = 'some text' assert_equal('some text', f.value) assert_equal('some text', f.value_raw) f['encoding'] = :b64 assert_equal('some text', f.value) assert_equal([ 'some text' ].pack('m*').chomp, f.value_raw) end def test_field_wrapping assert_equal("0:x\n", Vpim::DirectoryInfo::Field.create('0', 'x' * 1).encode(4)) assert_equal("0:xx\n", Vpim::DirectoryInfo::Field.create('0', 'x' * 2).encode(4)) assert_equal("0:xx\n x\n", Vpim::DirectoryInfo::Field.create('0', 'x' * 3).encode(4)) assert_equal("0:xx\n xx\n", Vpim::DirectoryInfo::Field.create('0', 'x' * 4).encode(4)) assert_equal("0:xx\n xxxx\n", Vpim::DirectoryInfo::Field.create('0', 'x' * 6).encode(4)) assert_equal("0:xx\n xxxx\n x\n", Vpim::DirectoryInfo::Field.create('0', 'x' * 7).encode(4)) end end vpim-0.695/test/test_ical.rb0000755000076500000240000002447411152674120014443 0ustar samstaff#!/usr/bin/env ruby require 'vpim/icalendar' require 'test/unit' # Sorry for the donkey patching... module Enumerable def first find{true} end def last inject{|memo, o| o} end end include Vpim Req_1 =<<___ BEGIN:VCALENDAR METHOD:REQUEST PRODID:-//Lotus Development Corporation//NONSGML Notes 6.0//EN VERSION:2.0 X-LOTUS-CHARSET:UTF-8 BEGIN:VTIMEZONE TZID:Pacific BEGIN:STANDARD DTSTART:19501029T020000 TZOFFSETFROM:-0700 TZOFFSETTO:-0800 RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=10 END:STANDARD BEGIN:DAYLIGHT DTSTART:19500402T020000 TZOFFSETFROM:-0800 TZOFFSETTO:-0700 RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=1SU;BYMONTH=4 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN="Gary Pope/Certicom" ;RSVP=FALSE:mailto:gpope@certicom.com ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION ;CN="Mike Harvey/Certicom";RSVP=TRUE:mailto:MHarvey@certicom.com ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE :mailto:rgallant@emilpost.certicom.com ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION ;CN="Sam Roberts/Certicom";RSVP=TRUE:mailto:SRoberts@certicom.com ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION ;CN="Tony Walters/Certicom";RSVP=TRUE:mailto:TWalters@certicom.com CLASS:PUBLIC DTEND;TZID="Pacific":20040415T130000 DTSTAMP:20040319T205045Z DTSTART;TZID="Pacific":20040415T120000 ORGANIZER;CN="Gary Pope/Certicom":mailto:gpope@certicom.com SEQUENCE:0 SUMMARY:hjold intyel TRANSP:OPAQUE UID:3E19204063C93D2388256E5C006BF8D9-Lotus_Notes_Generated X-LOTUS-BROADCAST:FALSE X-LOTUS-CHILD_UID:3E19204063C93D2388256E5C006BF8D9 X-LOTUS-NOTESVERSION:2 X-LOTUS-NOTICETYPE:I X-LOTUS-UPDATE-SEQ:1 X-LOTUS-UPDATE-WISL:$S:1;$L:1;$B:1;$R:1;$E:1 END:VEVENT END:VCALENDAR ___ Rep_1 =<<___ BEGIN:VCALENDAR CALSCALE:GREGORIAN PRODID:-//Apple Computer\, Inc//iCal 1.5//EN VERSION:2.0 METHOD:REPLY BEGIN:VEVENT ATTENDEE;CN="Sam Roberts/Certicom";PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPAN T;RSVP=TRUE:mailto:SRoberts@certicom.com CLASS:PUBLIC DTEND;TZID=Pacific:20040415T130000 DTSTAMP:20040319T205045Z DTSTART;TZID=Pacific:20040415T120000 ORGANIZER;CN="Gary Pope/Certicom":mailto:gpope@certicom.com SEQUENCE:0 SUMMARY:hjold intyel TRANSP:OPAQUE UID:3E19204063C93D2388256E5C006BF8D9-Lotus_Notes_Generated X-LOTUS-BROADCAST:FALSE X-LOTUS-CHILDUID:3E19204063C93D2388256E5C006BF8D9 X-LOTUS-NOTESVERSION:2 X-LOTUS-NOTICETYPE:I X-LOTUS-UPDATE-SEQ:1 X-LOTUS-UPDATE-WISL:$S:1\;$L:1\;$B:1\;$R:1\;$E:1 END:VEVENT END:VCALENDAR ___ class TestIcal < Test::Unit::TestCase def assert_time(expected, actual, msg = nil) assert_in_delta(expected, actual, 1, msg) end # Reported by Kyle Maxwell def test_serialize_todo icstodo =<<___ BEGIN:VCALENDAR VERSION:2.0 BEGIN:VTODO END:VTODO END:VCALENDAR ___ cal = Icalendar.decode(icstodo) assert_equal(icstodo, cal.to_s) end # Tracker #18920 def test_recurring_todos icstodo =<<___ BEGIN:VCALENDAR VERSION:2.0 BEGIN:VTODO SUMMARY:todo DTSTART:20040415T120000 RRULE:FREQ=WEEKLY;COUNT=2 END:VTODO END:VCALENDAR ___ cal = Icalendar.decode(icstodo).first todo = cal.todos.first assert(todo) assert_equal(todo.occurrences.to_a.size, 2) end def test_1 req = Icalendar.decode(Req_1).first req.components { } req.components(Icalendar::Vevent) { } req.components(Icalendar::Vjournal) { } assert_equal(1, req.components.size) assert_equal(1, req.components(Icalendar::Vevent).size) assert_equal(0, req.components(Icalendar::Vjournal).size) assert_equal(req.protocol, 'REQUEST') event = req.events.first assert(event) assert( event.attendee?('mailto:sroberts@certicom.com')) assert(!event.attendee?('mailto:sroberts@uniserve.com')) me = event.attendees('mailto:sroberts@certicom.com').first assert(me) assert(me == 'mailto:sroberts@certicom.com') reply = Icalendar.create_reply reply.push(event.accept(me)) # puts "Reply=>" # puts reply.to_s end def test_hal1 # Hal was encoding raw strings, here's how to do it with the API. cal = Icalendar.create start = Time.now event = Icalendar::Vevent.create(start, 'DTEND' => start + 60 * 60, 'SUMMARY' => "this is an event", 'RRULE' => 'freq=monthly;byday=2fr,4fr;count=5' ) cal.push event #puts cal.encode end # FIXME - test bad durations, like 'PT1D' def test_event_duration now = Time.now event = Icalendar::Vevent.create(now) assert_time(now, event.dtstart) assert_nil(event.duration) assert_nil(event.dtend) event = Icalendar::Vevent.create(Date.new(2000, 1, 21), 'DURATION' => 'PT1H') assert_equal(Time.gm(2000, 1, 21, 1), event.dtend) event = Icalendar::Vevent.create(Date.new(2000, 1, 21), 'DTEND' => Date.new(2000, 1, 22)) assert_equal(24*60*60, event.duration) end def test_todo_duration todo = Icalendar::Vtodo.create() assert_nil(todo.dtstart) assert_nil(todo.duration) assert_nil(todo.due) todo = Icalendar::Vtodo.create('DTSTART' => Date.new(2000, 1, 21), 'DURATION' => 'PT1H') assert_equal(Time.gm(2000, 1, 21, 1), todo.due) todo = Icalendar::Vtodo.create('DTSTART' => Date.new(2000, 1, 21), 'DUE' => Date.new(2000, 1, 22)) assert_equal(24*60*60, todo.duration) end def test_journal_create vj = Icalendar::Vjournal.create('DESCRIPTION' => "description") assert_equal("description", vj.description) end def TODO_test_occurrence_with_date_start d = Date.new(2000, 1, 21) event = Icalendar::Vevent.create(d) d1 = event.occurences.to_a.first assert_equal(d.class, d1.class) end def test_decode_duration_four_weeks assert_equal 4*7*86400, Icalendar.decode_duration('P4W') end def test_decode_duration_negative_two_weeks assert_equal(-2*7*86400, Icalendar.decode_duration('-P2W')) end def test_decode_duration_five_days assert_equal 5*86400, Icalendar.decode_duration('P5D') end def test_decode_duration_one_hour assert_equal 3600, Icalendar.decode_duration('PT1H') end def test_decode_duration_five_minutes assert_equal 5*60, Icalendar.decode_duration('PT5M') end def test_decode_duration_ten_seconds assert_equal 10, Icalendar.decode_duration('PT10S') end def test_decode_duration_with_leading_plus assert_equal 10, Icalendar.decode_duration('+PT10S') end def test_decode_duration_with_composite_duration assert_equal((15*86400+5*3600+20), Icalendar.decode_duration('P15DT5H0M20S')) end def test_create_with_prodid prodid = "me//here/non-sgml" cal = Icalendar.create2(prodid) do |cal| assert_respond_to(cal, :push) end assert_equal(prodid, cal.producer) end def test_occurences t0 = Time.now vc = Icalendar.create2 do |vc| vc.add_event do |ve| ve.dtstart t0 end end ve = vc.components(Icalendar::Vevent).first assert_time(t0, ve.occurences.select{true}.first) ve.occurences do |t| assert_time(t0, t) end vc = Icalendar.decode(<<__).first BEGIN:VCALENDAR BEGIN:VEVENT END:VEVENT END:VCALENDAR __ ve = vc.components(Icalendar::Vevent).first assert_raises(ArgumentError) { ve.occurences } end def test_each vc = Icalendar.decode(<<__).first BEGIN:VCALENDAR BEGIN:VEVENT END:VEVENT BEGIN:VTODO END:VTODO BEGIN:VJOURNAL END:VJOURNAL BEGIN:VTIMEZONE END:VTIMEZONE BEGIN:X-UNKNOWN END:X-UNKNOWN END:VCALENDAR __ count = Hash.new(0) vc.each do |c| count[c.class] += 1 end assert_equal(3, count.size) count.each do |_,v| assert_equal(1, v) end count = Hash.new(0) vc.events do |c| count[c.class] += 1 end vc.todos do |c| count[c.class] += 1 end vc.journals do |c| count[c.class] += 1 end assert_equal(3, count.size) count.each do |_,v| assert_equal(1, v) end assert_equal(3, vc.each.to_a.size) assert_equal(1, vc.each.select{|c| Vpim::Icalendar::Vevent === c}.size) assert_equal(1, vc.each.select{|c| Vpim::Icalendar::Vtodo === c}.size) assert_equal(1, vc.each.select{|c| Vpim::Icalendar::Vjournal === c}.size) assert_equal(1, vc.events.to_a.size) assert_equal(1, vc.todos.to_a.size) assert_equal(1, vc.journals.to_a.size) vc.to_s # Shouldn't raise... # TODO - encode isn't round-tripping, unknown components are lost, which is # not good. end def test_calscale req = Icalendar.decode(<<__).first BEGIN:VCALENDAR END:VCALENDAR __ assert_equal("GREGORIAN", req.calscale) req = Icalendar.decode(<<__).first BEGIN:VCALENDAR CALSCALE:GREGORIAN END:VCALENDAR __ assert_equal("GREGORIAN", req.calscale) end def test_version req = Icalendar.decode(<<__).first BEGIN:VCALENDAR END:VCALENDAR __ assert_raises(InvalidEncodingError) { req.version } req = Icalendar.decode(<<__).first BEGIN:VCALENDAR VERSION:2.0 END:VCALENDAR __ assert_equal(20, req.version) end def test_protocol req = Icalendar.decode(<<__).first BEGIN:VCALENDAR METHOD:GET END:VCALENDAR __ assert(req.protocol?("get")) assert(!req.protocol?("set")) end def test_transparency transparency = Icalendar.decode(<<__).first.to_a.first.transparency BEGIN:VCALENDAR BEGIN:VEVENT END:VEVENT END:VCALENDAR __ assert_equal("OPAQUE", transparency, "check default") transparency = Icalendar.decode(<<__).first.to_a.first.transparency BEGIN:VCALENDAR BEGIN:VEVENT TRANSP:Opaque END:VEVENT END:VCALENDAR __ assert_equal("OPAQUE", transparency, "check opaque") transparency = Icalendar.decode(<<__).first.to_a.first.transparency BEGIN:VCALENDAR BEGIN:VEVENT TRANSP:TrANSPARENT END:VEVENT END:VCALENDAR __ assert_equal("TRANSPARENT", transparency, "check transparent") end def test_location cal = Icalendar.decode(<<__).first BEGIN:VCALENDAR BEGIN:VEVENT LOCATION:bien located END:VEVENT BEGIN:VTODO LOCATION: END:VTODO BEGIN:VEVENT END:VEVENT END:VCALENDAR __ assert_equal(cal.events.first.location, "bien located") assert_equal(cal.todos.first.location, "") assert_equal(cal.events.last.location, nil) end def test_event_maker_w_rrule vc = Icalendar.create2 do |vc| vc.add_event do |m| m.add_rrule("freq=monthly") m.set_rrule do |_| _.frequency = "daily" end end end assert_no_match(/RRULE:FREQ=MONTHLY/, vc.to_s) assert_match(/RRULE:FREQ=DAILY/, vc.to_s) end end vpim-0.695/test/test_repo.rb0000644000076500000240000000470311152674120014466 0ustar samstaff#!/usr/bin/env ruby require 'vpim/repo' require 'test/common' class TestRepo < Test::Unit::TestCase Apple3 = Vpim::Repo::Apple3 Directory = Vpim::Repo::Directory Uri = Vpim::Repo::Uri def setup @testdir = Dir.getwd + "/test" #File.dirname($0) doesn't work with rcov :-( @caldir = @testdir + "/calendars" @eventsz = Dir[@caldir + "/**/*.ics"].size assert(@testdir) assert(test(?d, @caldir), "no caldir "+@caldir) end def _test_each(repo, eventsz) repo.each do |cal| assert_equal(eventsz, cal.events.count, cal.name) assert("", File.extname(cal.name)) assert_equal(cal.displayed, true) cal.events do |c| assert_instance_of(Vpim::Icalendar::Vevent, c) end cal.events.each do |c| assert_instance_of(Vpim::Icalendar::Vevent, c) end assert_equal(0, cal.todos.count) assert(cal.encode) end end def test_apple3 repo = Apple3.new(@caldir) assert_equal(1, repo.count) _test_each(repo, @eventsz) end def test_dir assert(test(?d, @caldir)) repo = Directory.new(@caldir) assert_equal(@eventsz, repo.count) _test_each(repo, 1) end def test_uri caldata = open('test/calendars/weather.calendar/Events/1205042405-0-0.ics').read server = data_on_port(caldata, 9876) begin c = Uri::Calendar.new("http://localhost:9876") assert_equal(caldata, c.encode) repo = Uri.new("http://localhost:9876") assert_equal(1, repo.count) _test_each(repo, 1) ensure server.kill end end def test_uri_invalid assert_raises(ArgumentError) do Uri.new("url") end assert_raises(ArgumentError) do c = Uri::Calendar.new("url://localhost") end assert_raises(ArgumentError) do c = Uri::Calendar.new("https://localhost") end end def test_uri_unreachable assert_raises(SocketError) do r = Uri.new("http://example.example") c = r.find{true} c.events{} end assert_raises(Errno::ECONNREFUSED) do r = Uri.new("http://rubyforge.org:81") c = r.find{true} c.events{} end assert_raises(StandardError, Vpim::InvalidEncodingError) do r = Uri.new("http://rubyforge.org/lua-rocks-my-world") c = r.find{true} c.events{} end end def test_uri_unparseable assert_raises(Vpim::InvalidEncodingError) do r = Uri.new("http://example.com:80") c = r.find{true} c.events{} end end end vpim-0.695/test/test_rrule.rb0000755000076500000240000007047711152674120014670 0ustar samstaff#!/usr/bin/env ruby ENV['TZ'] = 'EST5EDT' require 'vpim/rrule' require 'vpim/icalendar' require 'test/unit' require 'pp' =begin class Date alias :inspect :to_s end =end class TestRrule < Test::Unit::TestCase Rrule = Vpim::Rrule #=begin # Comment out these if you want printing! def puts(*args) end def pp(*args) end #=end def parse_vec(vec) pp vec vec = vec.gsub(/.*#.*\(/, '(') pp vec vec = vec.split("\n") pp vec ovec = [] vec.each do |v| time = v[0,18] dates = v[18,v.length - 18] pp time pp dates time.gsub!(/\((\d\d\d\d) (\d):/) { "(#{$1} 0#{$2}:" } dates.split(';').each do |mondays| mon, days = mondays.split(' ') # debug mon # debug days days.split(',').each do |d| # debug d r = d.split '-' # debug r case r.length when 1 then r = [ r[0].to_i ] when 2 then r = r[0].to_i .. r[1].to_i else raise "don't grok #{d}" end r.each do |d0| # debug time, mon, d, d0 ovec << sprintf("%s%s %.2d", time, mon, d0) end end end end ovec end def Test(rule, dtstart = nil, expected = nil) puts "---> #{rule}" puts " #{dtstart}" if dtstart expected = parse_vec(expected) pp expected.length start = Time.now if dtstart start = Vpim::Rrule.time_from_rfc2425(dtstart) end rrule = Vpim::Rrule.new(start, rule) # debug rrule # count = 1 # rrule.each do |t| # puts format("count=%3d %s", count, t.to_s) # count += 1 # end got = rrule.map { |t| t.strftime("(%Y %I:%M %p %Z)%B %d") } if expected && got != expected puts "length: got=#{got.length} expected=#{expected.length}" (0..expected.length).each do |i| if(got[i] != expected[i]) puts sprintf("%d: %34s %s %s", i, got[i], got[i] == expected[i] ? '==' : '!=', expected[i]) end end #p got #p expected end assert_equal(expected, got) end def test_rfc2445_examples_daily # Daily for 10 occurrences: # # DTSTART;TZID=US-Eastern:19970902T090000 # RRULE:FREQ=DAILY;COUNT=10 # # ==> (1997 9:00 AM EDT)September 2-11 Test( 'FREQ=DAILY;COUNT=10', '19970902T090000', < (1997 9:00 AM EDT)September 2-11 VEC ) # Daily until December 24, 1997: # # DTSTART;TZID=US-Eastern:19970902T090000 # RRULE:FREQ=DAILY;UNTIL=19971224T000000Z # # ==> (1997 9:00 AM EDT)September 2-30;October 1-25 # (1997 9:00 AM EST)October 26-31;November 1-30;December 1-23 Test( 'FREQ=DAILY;UNTIL=19971224T000000Z', '19970902T090000', < (1997 9:00 AM EDT)September 2-30;October 1-25 # (1997 9:00 AM EST)October 26-31;November 1-30;December 1-23 VEC ); # Every other day - forever: # # DTSTART;TZID=US-Eastern:19970902T090000 # RRULE:FREQ=DAILY;INTERVAL=2 # ==> (1997 9:00 AM EDT)September2,4,6,8...24,26,28,30; # October 2,4,6...20,22,24 # (1997 9:00 AM EST)October 26,28,30;November 1,3,5,7...25,27,29; # Dec 1,3,... Test( 'FREQ=DAILY;INTERVAL=2;count=27', '19970902T090000', < (1997 9:00 AM EDT)September 2,4,6,8,10,12,14,16,18,20,22,24,26,28,30;October 2,4,6,8,10,12,14,16,18,20,22,24 VEC ) # Every 10 days, 5 occurrences: # # DTSTART;TZID=US-Eastern:19970902T090000 # RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5 # # ==> (1997 9:00 AM EDT)September 2,12,22;October 2,12 Test( 'FREQ=DAILY;COUNT=5;Interval=10', '19970902T090000', < (1997 9:00 AM EDT)September 2,12,22;October 2,12 VEC ) end def test_rfc2445_examples_yearly # # Everyday in January, for 3 years: # # DTSTART;TZID=US-Eastern:19980101T090000 # RRULE:FREQ=YEARLY;UNTIL=20000131T090000Z; # BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA # or # RRULE:FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1 # # ==> (1998 9:00 AM EDT)January 1-31 # (1999 9:00 AM EDT)January 1-31 # (2000 9:00 AM EDT)January 1-31 # FIXME - # I believe the UNTIL time, being in UTC, is BEFORE (2000 9:00 AM # EDT)January 31 , so the last date in the result vector is not valid. # # Also, January is in EST, not EDT! Test( 'FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA', '19980101T090000', < (1998 9:00 AM EST)January 1-31 # (1999 9:00 AM EST)January 1-31 # (2000 9:00 AM EST)January 1-30 VEC ) Test( 'FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1', '19980101T090000', < (1998 9:00 AM EST)January 1-31 # (1999 9:00 AM EST)January 1-31 # (2000 9:00 AM EST)January 1-30 VEC ) end def test_rfc2445_examples_weekly_for_10_occurrences # Weekly for 10 occurrences # # DTSTART;TZID=US-Eastern:19970902T090000 # RRULE:FREQ=WEEKLY;COUNT=10 # # ==> (1997 9:00 AM EDT)September 2,9,16,23,30;October 7,14,21 # (1997 9:00 AM EST)October 28;November 4 Test( 'FREQ=WEEKLY;COUNT=10', '19970902T090000', < (1997 9:00 AM EDT)September 2,9,16,23,30;October 7,14,21 # (1997 9:00 AM EST)October 28;November 4 VEC ) end def test_rfc2445_examples_weekly_until_dec24_1997 # Weekly until December 24, 1997 # # DTSTART;TZID=US-Eastern:19970902T090000 # RRULE:FREQ=WEEKLY;UNTIL=19971224T000000Z # # ==> (1997 9:00 AM EDT)September 2,9,16,23,30;October 7,14,21 # (1997 9:00 AM EST)October 28;November 4,11,18,25;December 2,9,16,23 Test( 'FREQ=WEEKLY;UNTIL=19971224T000000Z', '19970902T090000', < (1997 9:00 AM EDT)September 2,9,16,23,30;October 7,14,21 # (1997 9:00 AM EST)October 28;November 4,11,18,25;December 2,9,16,23 VEC ) end def test_rfc2445_examples_every_other_week_forever # Every other week - forever: # # DTSTART;TZID=US-Eastern:19970902T090000 # RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU # # ==> (1997 9:00 AM EDT)September 2,16,30;October 14 # (1997 9:00 AM EST)October 28;November 11,25;December 9,23 # (1998 9:00 AM EST)January 6,20;February # ... Test( 'FREQ=WEEKLY;INTERVAL=2;WKST=SU;count=11', '19970902T090000', < (1997 9:00 AM EDT)September 2,16,30;October 14 # (1997 9:00 AM EST)October 28;November 11,25;December 9,23 # (1998 9:00 AM EST)January 6,20 VEC ) end def test_rfc2445_examples_weekly_on_t_and_th_for_5_weeks # Weekly on Tuesday and Thursday for 5 weeks: # # DTSTART;TZID=US-Eastern:19970902T090000 # RRULE:FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH # or # # RRULE:FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH # # ==> (1997 9:00 AM EDT)September 2,4,9,11,16,18,23,25,30;October 2 Test( 'FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH', '19970902T090000', < (1997 9:00 AM EDT)September 2,4,9,11,16,18,23,25,30;October 2 VEC ) end def test_rfc2445_examples_every_other_week_m_w_f_until_dec24 # Every other week on Monday, Wednesday and Friday until December 24, # 1997, but starting on Tuesday, September 2, 1997: # # DTSTART;TZID=US-Eastern:19970902T090000 # RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU; # BYDAY=MO,WE,FR # ==> (1997 9:00 AM EDT)September 2,3,5,15,17,19,29;October # 1,3,13,15,17 # (1997 9:00 AM EST)October 27,29,31;November 10,12,14,24,26,28; # December 8,10,12,22 Test( 'FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR', '19970902T090000', < (1997 9:00 AM EDT)September 2,3,5,15,17,19,29;October 1,3,13,15,17 # (1997 9:00 AM EST)October 27,29,31;November 10,12,14,24,26,28;December 8,10,12,22 VEC ) end def test_rfc2445_examples_every_other_week_t_th_for_8 # Every other week on Tuesday and Thursday, for 8 occurrences: # # DTSTART;TZID=US-Eastern:19970902T090000 # RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH # # ==> (1997 9:00 AM EDT)September 2,4,16,18,30;October 2,14,16 Test( 'FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH', '19970902T090000', < (1997 9:00 AM EDT)September 2,4,16,18,30;October 2,14,16 VEC ) end def test_rfc2445_examples_monthly # Monthly on the 1st Friday for ten occurrences: # # DTSTART;TZID=US-Eastern:19970905T090000 # RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR # # ==> (1997 9:00 AM EDT)September 5;October 3 # (1997 9:00 AM EST)November 7;Dec 5 # (1998 9:00 AM EST)January 2;February 6;March 6;April 3 # (1998 9:00 AM EDT)May 1;June 5 Test( 'FREQ=MONTHLY;COUNT=10;BYDAY=1FR', '19970905T090000', < (1997 9:00 AM EDT)September 5;October 3 # (1997 9:00 AM EST)November 7;December 5 # (1998 9:00 AM EST)January 2;February 6;March 6;April 3 # (1998 9:00 AM EDT)May 1;June 5 VEC ) # Monthly on the 1st Friday until December 24, 1997: # # DTSTART;TZID=US-Eastern:19970905T090000 # RRULE:FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR # # ==> (1997 9:00 AM EDT)September 5;October 3 # (1997 9:00 AM EST)November 7;December 5 Test( 'FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR', '19970905T090000', < (1997 9:00 AM EDT)September 5;October 3 # (1997 9:00 AM EST)November 7;December 5 VEC ) # Every other month on the 1st and last Sunday of the month for 10 # occurrences: # # DTSTART;TZID=US-Eastern:19970907T090000 # RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU # # ==> (1997 9:00 AM EDT)September 7,28 # (1997 9:00 AM EST)November 2,30 # (1998 9:00 AM EST)January 4,25;March 1,29 # (1998 9:00 AM EDT)May 3,31 Test( 'FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU', '19970907T090000', < (1997 9:00 AM EDT)September 7,28 # (1997 9:00 AM EST)November 2,30 # (1998 9:00 AM EST)January 4,25;March 1,29 # (1998 9:00 AM EDT)May 3,31 VEC ) # Monthly on the second to last Monday of the month for 6 months: # # DTSTART;TZID=US-Eastern:19970922T090000 # RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=-2MO # # ==> (1997 9:00 AM EDT)September 22;October 20 # (1997 9:00 AM EST)November 17;December 22 # (1998 9:00 AM EST)January 19;February 16 Test( 'FREQ=MONTHLY;COUNT=6;BYDAY=-2MO', '19970922T090000', < (1997 9:00 AM EDT)September 22;October 20 # (1997 9:00 AM EST)November 17;December 22 # (1998 9:00 AM EST)January 19;February 16 VEC ) # Monthly on the third to the last day of the month, forever: # # DTSTART;TZID=US-Eastern:19970928T090000 # RRULE:FREQ=MONTHLY;BYMONTHDAY=-3 # # ==> (1997 9:00 AM EDT)September 28 # (1997 9:00 AM EST)October 29;November 28;December 29 # (1998 9:00 AM EST)January 29;February 26 # ... Test( 'FREQ=MONTHLY;BYMONTHDAY=-3;count=6', '19970928T090000', < (1997 9:00 AM EDT)September 28 # (1997 9:00 AM EST)October 29;November 28;December 29 # (1998 9:00 AM EST)January 29;February 26 VEC ) # Monthly on the 2nd and 15th of the month for 10 occurrences: # # DTSTART;TZID=US-Eastern:19970902T090000 # RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15 # # ==> (1997 9:00 AM EDT)September 2,15;October 2,15 # (1997 9:00 AM EST)November 2,15;December 2,15 # (1998 9:00 AM EST)January 2,15 Test( 'FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15', '19970902T090000', < (1997 9:00 AM EDT)September 2,15;October 2,15 # (1997 9:00 AM EST)November 2,15;December 2,15 # (1998 9:00 AM EST)January 2,15 VEC ) # Monthly on the first and last day of the month for 10 occurrences: # # DTSTART;TZID=US-Eastern:19970930T090000 # RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1 # # ==> (1997 9:00 AM EDT)September 30;October 1 # (1997 9:00 AM EST)October 31;November 1,30;December 1,31 # (1998 9:00 AM EST)January 1,31;February 1 Test( 'FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1', '19970930T090000', < (1997 9:00 AM EDT)September 30;October 1 # (1997 9:00 AM EST)October 31;November 1,30;December 1,31 # (1998 9:00 AM EST)January 1,31;February 1 VEC ) # Every 18 months on the 10th thru 15th of the month for 10 # occurrences: # # DTSTART;TZID=US-Eastern:19970910T090000 # RRULE:FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14, # 15 # # ==> (1997 9:00 AM EDT)September 10,11,12,13,14,15 # (1999 9:00 AM EST)March 10,11,12,13 Test( 'FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15', '19970910T090000', < (1997 9:00 AM EDT)September 10,11,12,13,14,15 # (1999 9:00 AM EST)March 10,11,12,13 VEC ) # Every Tuesday, every other month: # # DTSTART;TZID=US-Eastern:19970902T090000 # RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=TU # # ==> (1997 9:00 AM EDT)September 2,9,16,23,30 # (1997 9:00 AM EST)November 4,11,18,25 # (1998 9:00 AM EST)January 6,13,20,27;March 3,10,17,24,31 # ... Test( 'FREQ=MONTHLY;INTERVAL=2;BYDAY=TU;count=18', '19970902T090000', < (1997 9:00 AM EDT)September 2,9,16,23,30 # (1997 9:00 AM EST)November 4,11,18,25 # (1998 9:00 AM EST)January 6,13,20,27;March 3,10,17,24,31 VEC ) end def test_rfc2445_examples_misc1 # Yearly in June and July for 10 occurrences: # # DTSTART;TZID=US-Eastern:19970610T090000 # RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7 # ==> (1997 9:00 AM EDT)June 10;July 10 # (1998 9:00 AM EDT)June 10;July 10 # (1999 9:00 AM EDT)June 10;July 10 # (2000 9:00 AM EDT)June 10;July 10 # (2001 9:00 AM EDT)June 10;July 10 # Note: Since none of the BYDAY, BYMONTHDAY or BYYEARDAY components # are specified, the day is gotten from DTSTART Test( 'FREQ=YEARLY;COUNT=10;BYMONTH=6,7', '19970610T090000', < (1997 9:00 AM EDT)June 10;July 10 # (1998 9:00 AM EDT)June 10;July 10 # (1999 9:00 AM EDT)June 10;July 10 # (2000 9:00 AM EDT)June 10;July 10 # (2001 9:00 AM EDT)June 10;July 10 VEC ) # Every other year on January, February, and March for 10 occurrences: # # DTSTART;TZID=US-Eastern:19970310T090000 # RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3 # # ==> (1997 9:00 AM EST)March 10 # (1999 9:00 AM EST)January 10;February 10;March 10 # (2001 9:00 AM EST)January 10;February 10;March 10 # (2003 9:00 AM EST)January 10;February 10;March 10 Test( 'FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3', '19970310T090000', < (1997 9:00 AM EST)March 10 # (1999 9:00 AM EST)January 10;February 10;March 10 # (2001 9:00 AM EST)January 10;February 10;March 10 # (2003 9:00 AM EST)January 10;February 10;March 10 VEC ) # Every 3rd year on the 1st, 100th and 200th day for 10 occurrences: # # DTSTART;TZID=US-Eastern:19970101T090000 # RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200 # # ==> (1997 9:00 AM EST)January 1 # (1997 9:00 AM EDT)April 10;July 19 # (2000 9:00 AM EST)January 1 # (2000 9:00 AM EDT)April 9;July 18 # (2003 9:00 AM EST)January 1 # (2003 9:00 AM EDT)April 10;July 19 # (2006 9:00 AM EST)January 1 Test( 'FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200', '19970101T090000', < (1997 9:00 AM EST)January 1 # (1997 9:00 AM EDT)April 10;July 19 # (2000 9:00 AM EST)January 1 # (2000 9:00 AM EDT)April 9;July 18 # (2003 9:00 AM EST)January 1 # (2003 9:00 AM EDT)April 10;July 19 # (2006 9:00 AM EST)January 1 VEC ) # Every 20th Monday of the year, forever: # DTSTART;TZID=US-Eastern:19970519T090000 # RRULE:FREQ=YEARLY;BYDAY=20MO # # ==> (1997 9:00 AM EDT)May 19 # (1998 9:00 AM EDT)May 18 # (1999 9:00 AM EDT)May 17 # ... Test( 'FREQ=YEARLY;BYDAY=20MO;count=3', '19970519T090000', < (1997 9:00 AM EDT)May 19 # (1998 9:00 AM EDT)May 18 # (1999 9:00 AM EDT)May 17 VEC ) # Monday of week number 20 (where the default start of the week is # Monday), forever: # # DTSTART;TZID=US-Eastern:19970512T090000 # RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO # # ==> (1997 9:00 AM EDT)May 12 # (1998 9:00 AM EDT)May 11 # (1999 9:00 AM EDT)May 17 # ... # TODO # Every Thursday in March, forever: # # DTSTART;TZID=US-Eastern:19970313T090000 # RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=TH # # ==> (1997 9:00 AM EST)March 13,20,27 # (1998 9:00 AM EST)March 5,12,19,26 # (1999 9:00 AM EST)March 4,11,18,25 # ... Test( 'FREQ=YEARLY;BYMONTH=3;BYDAY=TH;count=11', '19970313T090000', < (1997 9:00 AM EST)March 13,20,27 # (1998 9:00 AM EST)March 5,12,19,26 # (1999 9:00 AM EST)March 4,11,18,25 VEC ) # Every Thursday, but only during June, July, and August, forever: # # DTSTART;TZID=US-Eastern:19970605T090000 # RRULE:FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8 # # ==> (1997 9:00 AM EDT)June 5,12,19,26;July 3,10,17,24,31; # August 7,14,21,28 # (1998 9:00 AM EDT)June 4,11,18,25;July 2,9,16,23,30; # August 6,13,20,27 # (1999 9:00 AM EDT)June 3,10,17,24;July 1,8,15,22,29; # August 5,12,19,26 # ... Test( 'FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8;count=39', '19970605T090000', < (1997 9:00 AM EDT)June 5,12,19,26;July 3,10,17,24,31;August 7,14,21,28 # (1998 9:00 AM EDT)June 4,11,18,25;July 2,9,16,23,30;August 6,13,20,27 # (1999 9:00 AM EDT)June 3,10,17,24;July 1,8,15,22,29;August 5,12,19,26 VEC ) =begin EXDATE isn't supported. # Every Friday the 13th, forever: # # DTSTART;TZID=US-Eastern:19970902T090000 # EXDATE;TZID=US-Eastern:19970902T090000 # RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13 # ==> (1998 9:00 AM EST)February 13;March 13;November 13 # (1999 9:00 AM EDT)August 13 # (2000 9:00 AM EDT)October 13 # ... Test( 'FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13;count=5', '19970902T090000', < (1998 9:00 AM EST)February 13;March 13;November 13 # (1999 9:00 AM EDT)August 13 # (2000 9:00 AM EDT)October 13 VEC ) =end # The first Saturday that follows the first Sunday of the month, # forever: # # DTSTART;TZID=US-Eastern:19970913T090000 # RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13 # # ==> (1997 9:00 AM EDT)September 13;October 11 # (1997 9:00 AM EST)November 8;December 13 # (1998 9:00 AM EST)January 10;February 7;March 7 # (1998 9:00 AM EDT)April 11;May 9;June 13... # ... Test( 'FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13;count=10', '19970913T090000', < (1997 9:00 AM EDT)September 13;October 11 # (1997 9:00 AM EST)November 8;December 13 # (1998 9:00 AM EST)January 10;February 7;March 7 # (1998 9:00 AM EDT)April 11;May 9;June 13... VEC ) # Every four years, the first Tuesday after a Monday in November, # forever (U.S. Presidential Election day): # # DTSTART;TZID=US-Eastern:19961105T090000 # RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4, # 5,6,7,8 # # ==> (1996 9:00 AM EST)November 5 # (2000 9:00 AM EST)November 7 # (2004 9:00 AM EST)November 2 # ... Test( 'FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8;count=3', '19961105T090000', < (1996 9:00 AM EST)November 5 # (2000 9:00 AM EST)November 7 # (2004 9:00 AM EST)November 2 VEC ) # The 3rd instance into the month of one of Tuesday, Wednesday or # Thursday, for the next 3 months: # # DTSTART;TZID=US-Eastern:19970904T090000 # RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3 # # ==> (1997 9:00 AM EDT)September 4;October 7 # (1997 9:00 AM EST)November 6 Test( 'FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3', '19970904T090000', < (1997 9:00 AM EDT)September 4;October 7 # (1997 9:00 AM EST)November 6 VEC ) end def test_rfc2445_example_2nd_last_weekday_of_month # The 2nd to last weekday of the month: # # DTSTART;TZID=US-Eastern:19970929T090000 # RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2 # # ==> (1997 9:00 AM EDT)September 29 # (1997 9:00 AM EST)October 30;November 27;December 30 # (1998 9:00 AM EST)January 29;February 26;March 30 # ... # Test( 'FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2;count=7', '19970929T090000', < (1997 9:00 AM EDT)September 29 # (1997 9:00 AM EST)October 30;November 27;December 30 # (1998 9:00 AM EST)January 29;February 26;March 30 VEC ) end def test_rfc2445_examples_misc2 # Every 3 hours from 9:00 AM to 5:00 PM on a specific day: # # DTSTART;TZID=US-Eastern:19970902T090000 # RRULE:FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000Z # # ==> (September 2, 1997 EDT)09:00,12:00,15:00 # # Every 15 minutes for 6 occurrences: # # DTSTART;TZID=US-Eastern:19970902T090000 # RRULE:FREQ=MINUTELY;INTERVAL=15;COUNT=6 # # ==> (September 2, 1997 EDT)09:00,09:15,09:30,09:45,10:00,10:15 # # Every hour and a half for 4 occurrences: # # DTSTART;TZID=US-Eastern:19970902T090000 # RRULE:FREQ=MINUTELY;INTERVAL=90;COUNT=4 # # ==> (September 2, 1997 EDT)09:00,10:30;12:00;13:30 # # Every 20 minutes from 9:00 AM to 4:40 PM every day: # # DTSTART;TZID=US-Eastern:19970902T090000 # RRULE:FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40 # or # RRULE:FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16 # # ==> (September 2, 1997 EDT)9:00,9:20,9:40,10:00,10:20, # ... 16:00,16:20,16:40 # (September 3, 1997 EDT)9:00,9:20,9:40,10:00,10:20, # ...16:00,16:20,16:40 # ... end def test_rfc2445_examples_weekly_days_differ_on_wkst # An example where the days generated makes a difference because of # WKST: # # DTSTART;TZID=US-Eastern:19970805T090000 # RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO # # ==> (1997 EDT)Aug 5,10,19,24 Test( 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO', '19970805T090000', < (1997 9:00 AM EDT)August 5,10,19,24 VEC ) # # changing only WKST from MO to SU, yields different results... # # DTSTART;TZID=US-Eastern:19970805T090000 # RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU # ==> (1997 EDT)August 5,17,19,31 Test( 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU', '19970805T090000', < (1997 9:00 AM EDT)August 5,17,19,31 VEC ) end def test_us_laborday =begin Patch with test from Sam Stephenson at 37signals: We're using your vPim library at 37signals for the Backpack Calendar (http://backpackit.com/calendar ) and ran into an issue with the "US Holidays" calendar available at http://ical.mac.com/ical/US32Holidays.ics . Specifically, calling Vpim::Rrule#each for events such as Labor Day: DTSTART;VALUE=DATE:20020902 DTEND;VALUE=DATE:20020903 RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=9;BYDAY=1MO would never yield any recurrences. =end # The first Monday in September, forever (Labor Day): # # DTSTART;TZID=US-Eastern:20020902T090000 # RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=9;BYDAY=1MO # # ==> (2002 9:00 AM EST)September 2 # (2003 9:00 AM EST)September 1 # (2004 9:00 AM EST)September 6 # ... Test( 'FREQ=YEARLY;INTERVAL=1;BYMONTH=9;BYDAY=1MO;count=3', '20020902T090000', < (2002 9:00 AM EDT)September 2 # (2003 9:00 AM EDT)September 1 # (2004 9:00 AM EDT)September 6 VEC ) end def test_zipdx_weekly_1 # Example provided by Zipdx # Produced by: Zimbra-Calendar-Provider # Interop tested against Apple iCal 3.0.2 Test( 'FREQ=WEEKLY;UNTIL=20080501;INTERVAL=1;BYDAY=TU,TH', '20080415T160000', < (2008 4:00 PM EDT)April 15,17,22,24,29 VEC ); end def test_zipdx_weekly_2 # Example provided by Zipdx # Produced by: -//Microsoft Corporation//Outlook 11.0 MIMEDIR//EN # Interop tested against Apple iCal 3.0.2 Test( 'FREQ=WEEKLY;COUNT=9;INTERVAL=2;BYDAY=MO,WE,FR;WKST=SU', '20080811T130000', < (2008 1:00 PM EDT)August 11,13,15,25,27,29;September 8,10,12 VEC ); end def test_zipdx_daily_1 # Example provided by Zipdx # Produced by: Microsoft CDO for Microsoft Exchange # Interop tested against Apple iCal 3.0.2 Test( 'FREQ=DAILY;COUNT=7;WKST=SU;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR', '20080218T180000', < (2008 6:00 PM EST)February 18,19,20,21,22,25,26 VEC ); end def test_bysetpos_before_dtstart # Note - this doesn't work with Apple iCal 3.0.2, I think its their bug. Test( 'FREQ=MONTHLY;COUNT=5;BYDAY=MO;BYSETPOS=1,2', '20080305T180000', < (2008 6:00 PM EST)March 5 # (2008 6:00 PM EDT)March 10 # (2008 6:00 PM EDT)April 7,14 # (2008 6:00 PM EDT)May 5 VEC ); end def test_bysetpos_after_until Test( 'FREQ=MONTHLY;UNTIL=20080421;BYDAY=MO;BYSETPOS=-1', '20080305T180000', < (2008 6:00 PM EST)March 5 # (2008 6:00 PM EDT)March 31 VEC ); end def test_bysetpos_zipdx_last_saturday # In Microsoft Exchange, if I want a meeting to occur on a certain Saturday # of each month, Exchange generates: # # RRULE:FREQ=MONTHLY;COUNT=4;WKST=SU;INTERVAL=1;BYDAY=-1SA # # However, if I use Microsoft Outlook, Outlook generates: # RRULE:FREQ=MONTHLY;COUNT=4;INTERVAL=1;BYDAY=SA;BYSETPOS=-1;WKST=SU # And we get confused and generate the meeting every week (instead of every # month). I think this is because the library does not support BYSETPOS; # this is stated in the documentation. Test( 'FREQ=MONTHLY;COUNT=4;INTERVAL=1;WKST=SU;BYDAY=SA;BYSETPOS=-1', '20080305T180000', < (2008 6:00 PM EST)March 5 # (2008 6:00 PM EDT)March 29 # (2008 6:00 PM EDT)April 26 # (2008 6:00 PM EDT)May 31 VEC ); end =begin BEGIN:VEVENT SUMMARY:Boxing Day DESCRIPTION:First Weekday on or after December 26th. DTSTAMP:20030701T000000Z UID:holiday0042@icaldates.com CATEGORIES:Holiday - Canada DTSTART;VALUE=DATE:17531226 RRULE:FREQ=MONTHLY;BYMONTH=12;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;BYMONTHDAY=26,27,28;BYSETPOS=1 END:VEVENT =end def test_reccurrence_with_utc_dtstart # Its wrong that the times yielded aren't in the timezone of DTSTART, but # until vPim supports timezones, its the best it'll get. txt = <<'__' BEGIN:VCALENDAR BEGIN:VEVENT DTSTAMP:20080416T174954Z ORGANIZER;CN=Anonymous:MAILTO:ano@nymo.us CREATED:20080401T090904Z LAST-MODIFIED:20080401T090904Z SUMMARY:Very important recurring event RRULE:FREQ=WEEKLY;UNTIL=20080415T093000Z;BYDAY=TU;BYHOUR=9 DTSTART:20080401T093000Z DTEND:20080401T110000Z TRANSP:OPAQUE END:VEVENT END:VCALENDAR __ cal = Vpim::Icalendar.decode(txt).first occurs = cal.events.to_a.first.occurrences.to_a #p occurs utc = occurs.map{|y| y.utc} #p utc expects = [ Time.utc(2008, 4, 1, 9, 30), Time.utc(2008, 4, 8, 9, 30), Time.utc(2008, 4,15, 9, 30), ] assert_equal(expects, utc) end def test_maker assert_equal("FREQ=WEEKLY", Rrule::Maker.new{|m|m.frequency = "WEEKLY"}.encode) assert_equal("FREQ=WEEKLY;COUNT=2", Rrule::Maker.new{|m|m.frequency = "WEEKLY"; m.count = 2}.encode) assert_raises(ArgumentError) do Rrule::Maker.new{|m|m.count = 2; m.until = Time.now} end assert_raises(ArgumentError) do Rrule::Maker.new{|m|m.until = Time.now; m.count = 4} end assert_raises(ArgumentError) do Rrule::Maker.new.encode end end end vpim-0.695/test/test_vcard.rb0000755000076500000240000006265111152674120014631 0ustar samstaff#!/usr/bin/env ruby require 'vpim/vcard' require 'test/unit' require 'date' require 'pp' include Vpim # Test equivalence where whitespace is compressed. def assert_equal_nospace(expected, got) expected = expected.gsub(/\s+/,'') got = expected.gsub(/\s+/,'') assert_equal(expected, got) end # Test cases: multiple occurrences of type =begin begin:VCARD version:2.1 v;x1=a;x2=,a;x3=a,;x4=a,,a;x5=,a,: source:ldap://cn=bjorn%20Jensen, o=university%20of%20Michigan, c=US fn:Bj=F8rn Jensen other.name:Jensen;Bj=F8rn some.other.value:1.2.3 some.other.value:some.other some.other.value:some.other.value v;p-1=;p-2=,,;p-3=a;p-4=a b,"v;p-1=;p-2=,,;p-3=a;p-4=a":v-value email;type=internet: bjorn@umich.edu tel;type=work,voice,msg:+1 313 747-4454 tel:+... key;type=x509;encoding=B:dGhpcyBjb3VsZCBiZSAKbXkgY2VydGlmaWNhdGUK end:vcard =end class TestVcard < Test::Unit::TestCase # RFC2425 - 8.1. Example 1 # Note that this is NOT a valid vCard, it lacks BEGIN/END. EX1 =<<'EOF' cn: cn:Babs Jensen cn:Barbara J Jensen sn:Jensen email:babs@umich.edu phone:+1 313 747-4454 x-id:1234567890 EOF def test_ex1 card = nil ex1 = EX1 assert_nothing_thrown { card = Vpim::DirectoryInfo.decode(ex1) } assert_equal_nospace(EX1, card.to_s) assert_equal("Babs Jensen", card["cn"]) assert_equal("Jensen", card["sn"]) assert_equal("babs@umich.edu", card[ "email" ]) assert_equal("+1 313 747-4454", card[ "PhOnE" ]) assert_equal("1234567890", card[ "x-id" ]) assert_equal([], card.groups) end # RFC2425 - 8.2. Example 2 EX2 = <<-END begin:VCARD source:ldap://cn=bjorn%20Jensen, o=university%20of%20Michigan, c=US name:Bjorn Jensen fn:Bj=F8rn Jensen n:Jensen;Bj=F8rn email;type=internet:bjorn@umich.edu tel;type=work,voice,msg:+1 313 747-4454 key;type=x509;encoding=B:dGhpcyBjb3VsZCBiZSAKbXkgY2VydGlmaWNhdGUK end:VCARD END def test_ex2 card = nil ex2 = EX2 assert_nothing_thrown { card = Vpim::Vcard.decode(ex2).first } assert_equal(EX2, card.encode(0)) assert_raises(InvalidEncodingError) { card.version } assert_equal("Bj=F8rn Jensen", card.name.fullname) assert_equal("Jensen", card.name.family) assert_equal("Bj=F8rn", card.name.given) assert_equal("", card.name.prefix) assert_equal("Bj=F8rn Jensen", card[ "fn" ]) assert_equal("+1 313 747-4454", card[ "tEL" ]) assert_equal(nil, card[ "not-a-field" ]) assert_equal([], card.groups) assert_equal(nil, card.enum_by_name("n").entries[0].param("encoding")) assert_equal(["internet"], card.enum_by_name("Email").entries.first.param("Type")) assert_equal(nil, card.enum_by_name("Email").entries[0].param("foo")) assert_equal(["B"], card.enum_by_name("kEy").to_a.first.param("encoding")) assert_equal("B", card.enum_by_name("kEy").entries[0].encoding) assert_equal(["work", "voice", "msg"], card.enum_by_name("tel").entries[0].param("Type")) assert_equal([card.fields[6]], card.enum_by_name("tel").entries) assert_equal([card.fields[6]], card.enum_by_name("tel").to_a) assert_equal(nil, card.enum_by_name("tel").entries.first.encoding) assert_equal("B", card.enum_by_name("key").entries.first.encoding) assert_equal("dGhpcyBjb3VsZCBiZSAKbXkgY2VydGlmaWNhdGUK", card.enum_by_name("key").entries.first.value_raw) assert_equal("this could be \nmy certificate\n", card.enum_by_name("key").entries.first.value) card.lines end =begin EX3 = <<-END begin:vcard source:ldap://cn=Meister%20Berger,o=Universitaet%20Goerlitz,c=DE name:Meister Berger fn:Meister Berger n:Berger;Meister bday;value=date:1963-09-21 o:Universit=E6t G=F6rlitz title:Mayor title;language=de;value=text:Burgermeister note:The Mayor of the great city of Goerlitz in the great country of Germany. email;internet:mb@goerlitz.de home.tel;type=fax,voice,msg:+49 3581 123456 home.label:Hufenshlagel 1234\n 02828 Goerlitz\n Deutschland key;type=X509;encoding=b:MIICajCCAdOgAwIBAgICBEUwDQYJKoZIhvcNAQEEBQ AwdzELMAkGA1UEBhMCVVMxLDAqBgNVBAoTI05ldHNjYXBlIENvbW11bmljYXRpb25zI ENvcnBvcmF0aW9uMRwwGgYDVQQLExNJbmZvcm1hdGlvbiBTeXN0ZW1zMRwwGgYDVQQD ExNyb290Y2EubmV0c2NhcGUuY29tMB4XDTk3MDYwNjE5NDc1OVoXDTk3MTIwMzE5NDc 1OVowgYkxCzAJBgNVBAYTAlVTMSYwJAYDVQQKEx1OZXRzY2FwZSBDb21tdW5pY2F0aW 9ucyBDb3JwLjEYMBYGA1UEAxMPVGltb3RoeSBBIEhvd2VzMSEwHwYJKoZIhvcNAQkBF hJob3dlc0BuZXRzY2FwZS5jb20xFTATBgoJkiaJk/IsZAEBEwVob3dlczBcMA0GCSqG SIb3DQEBAQUAA0sAMEgCQQC0JZf6wkg8pLMXHHCUvMfL5H6zjSk4vTTXZpYyrdN2dXc oX49LKiOmgeJSzoiFKHtLOIboyludF90CgqcxtwKnAgMBAAGjNjA0MBEGCWCGSAGG+E IBAQQEAwIAoDAfBgNVHSMEGDAWgBT84FToB/GV3jr3mcau+hUMbsQukjANBgkqhkiG9 w0BAQQFAAOBgQBexv7o7mi3PLXadkmNP9LcIPmx93HGp0Kgyx1jIVMyNgsemeAwBM+M SlhMfcpbTrONwNjZYW8vJDSoi//yrZlVt9bJbs7MNYZVsyF1unsqaln4/vy6Uawfg8V UMk1U7jt8LYpo4YULU7UZHPYVUaSgVttImOHZIKi4hlPXBOhcUQ== end:vcard END assert_equal( ["other", "some.other"], card.groups_all ) #a = [] #card.enum_by_group("some.other").each { |field| a << field } #assert_equal(card.fields.indexes(6, 7, 8), a) #assert_equal(card.fields.indexes(6, 7, 8), card.fields_by_group("some.other")) =end # This is my vCard exported from OS X's AddressBook.app. EX_APPLE1 =<<'EOF' BEGIN:VCARD VERSION:3.0 N:Roberts;Sam;;; FN:Roberts Sam EMAIL;type=HOME;type=pref:sroberts@uniserve.com TEL;type=WORK;type=pref:905-501-3781 TEL;type=FAX:905-907-4230 TEL;type=HOME:416 535 5341 ADR;type=HOME;type=pref:;;376 Westmoreland Ave.;Toronto;ON;M6H 3 A6;Canada NOTE:CATEGORIES: Amis/Famille BDAY;value=date:1970-07-14 END:VCARD EOF def test_ex_apple1 card = nil assert_nothing_thrown { card = Vpim::Vcard.decode(EX_APPLE1).first } assert_equal("Roberts Sam", card.name.fullname) assert_equal("Roberts", card.name.family) assert_equal("Sam", card.name.given) assert_equal("", card.name.prefix) assert_equal("", card.name.suffix) assert_equal(EX_APPLE1, card.to_s(64)) check_ex_apple1(card) end NICKNAME0=<<'EOF' begin:vcard end:vcard EOF NICKNAME1=<<'EOF' begin:vcard nickname: end:vcard EOF NICKNAME2=<<'EOF' begin:vcard nickname: end:vcard EOF NICKNAME3=<<'EOF' begin:vcard nickname: Big Joey end:vcard EOF NICKNAME4=<<'EOF' begin:vcard nickname: nickname: Big Joey end:vcard EOF NICKNAME5=<<'EOF' begin:vcard nickname: nickname: Big Joey nickname:Bob end:vcard EOF def test_nickname assert_equal(nil, Vpim::Vcard.decode(NICKNAME0).first.nickname) assert_equal(nil, Vpim::Vcard.decode(NICKNAME1).first.nickname) assert_equal(nil, Vpim::Vcard.decode(NICKNAME2).first.nickname) assert_equal('Big Joey', Vpim::Vcard.decode(NICKNAME3).first.nickname) assert_equal('Big Joey', Vpim::Vcard.decode(NICKNAME4).first['nickname']) assert_equal(['Big Joey', 'Bob'], Vpim::Vcard.decode(NICKNAME5).first.nicknames) end def check_ex_apple1(card) assert_equal("3.0", card[ "version" ]) assert_equal(30, card.version) assert_equal("sroberts@uniserve.com", card[ "email" ]) assert_equal(["HOME", "pref"], card.enum_by_name("email").entries.first.param("type")) assert_equal(nil, card.enum_by_name("email").entries.first.group) assert_equal(["WORK","pref"], card.enum_by_name("tel").entries[0].param("type")) assert_equal(["FAX"], card.enum_by_name("tel").entries[1].param("type")) assert_equal(["HOME"], card.enum_by_name("tel").entries[2].param("type")) assert_equal(nil, card.enum_by_name("bday").entries[0].param("type")) assert_equal(["date"], card.enum_by_name("bday").entries[0].param("value")) assert_equal( 1970, card.enum_by_name("bday").entries[0].to_time[0].year) assert_equal( 7, card.enum_by_name("bday").entries[0].to_time[0].month) assert_equal( 14, card.enum_by_name("bday").entries[0].to_time[0].day) assert_equal("CATEGORIES: Amis/Famille", card[ "note" ]) end # Test data for Vpim.expand EX_EXPAND =<<'EOF' BEGIN:a a1: BEGIN:b BEGIN:c c1: c2: END:c V1: V2: END:b a2: END:a EOF def test_expand src = Vpim.decode(EX_EXPAND) dst = Vpim.expand(src) assert_equal('a', dst[0][0].value) assert_equal('A1', dst[0][1].name) assert_equal('b', dst[0][2][0].value) assert_equal('c', dst[0][2][1][0].value) assert_equal('C1', dst[0][2][1][1].name) assert_equal('C2', dst[0][2][1][2].name) assert_equal('c', dst[0][2][1][3].value) cards = Vpim::Vcard.decode(EX_APPLE1) assert_equal(1, cards.length) check_ex_apple1(cards[0]) end # An iCalendar for Vpim.expand EX_ICAL_1 =<<'EOF' BEGIN:VCALENDAR CALSCALE:GREGORIAN X-WR-TIMEZONE;VALUE=TEXT:Canada/Eastern METHOD:PUBLISH PRODID:-//Apple Computer\, Inc//iCal 1.0//EN X-WR-RELCALID;VALUE=TEXT:18E75B8C-5722-11D7-AB0B-000393AD088C X-WR-CALNAME;VALUE=TEXT:Events VERSION:2.0 BEGIN:VEVENT SEQUENCE:14 UID:18E74C28-5722-11D7-AB0B-000393AD088C DTSTAMP:20030301T171521Z SUMMARY:Bob Log III DTSTART;TZID=Canada/Eastern:20030328T200000 DTEND;TZID=Canada/Eastern:20030328T230000 DESCRIPTION:Healey's\n\nLook up exact time.\n BEGIN:VALARM TRIGGER;VALUE=DURATION:-P2D ACTION:DISPLAY DESCRIPTION:Event reminder END:VALARM BEGIN:VALARM ATTENDEE:mailto:sroberts@uniserve.com TRIGGER;VALUE=DURATION:-P1D ACTION:EMAIL SUMMARY:Alarm notification DESCRIPTION:This is an event reminder END:VALARM END:VEVENT BEGIN:VEVENT SEQUENCE:1 DTSTAMP:20030312T043534Z SUMMARY:Small Potatoes 10\nFriday\, March 14th\, 8:00 p.m.\n361 Danforth Avenue (at Hampton -- Chester subway)\nInfo:  (416) 480-2802 or (416) 323-1715\n UID:18E750A8-5722-11D7-AB0B-000393AD088C DTSTART;TZID=Canada/Eastern:20030315T000000 DURATION:PT1H BEGIN:VALARM ATTENDEE:mailto:sroberts@uniserve.com TRIGGER;VALUE=DURATION:-P1D ACTION:EMAIL SUMMARY:Alarm notification DESCRIPTION:This is an event reminder END:VALARM END:VEVENT END:VCALENDAR EOF def test_ical_1 src = nil dst = nil assert_nothing_thrown { src = Vpim.decode(EX_ICAL_1) dst = Vpim.expand(src) } #p dst end # Constructed data. TST1 =<<'EOF' BEGIN:vCard DESCRIPTION:Healey's\n\nLook up exact time.\n email;type=work:work@example.com email;type=internet,home;type=pref:home@example.com fax;type=foo,pref;bar:fax name:firstname name:secondname time;value=time: END:vCARD EOF def _test_cons # FIXME card = nil assert_nothing_thrown { card = Vpim::Vcard.decode(TST1).first } assert_equal(TST1, card.to_s) assert_equal('Healey\'s\n\nLook up exact time.\n', card[ "description" ]) # Test the [] API assert_equal(nil, card[ "not-a-field" ]) assert_equal('firstname', card[ "name" ]) assert_equal('home@example.com', card[ "email" ]) assert_equal('home@example.com', card[ "email", "pref" ]) assert_equal('home@example.com', card[ "email", "internet" ]) assert_equal('work@example.com', card[ "email", "work" ]) # Test the merging of vCard 2.1 type fields. #p card #p card.enum_by_name('fax').entries[0].each_param { |p,v| puts "#{p} = #{v}\n" } assert_equal('fax', card[ "fax" ]) assert_equal('fax', card[ "fax", 'bar' ]) end =begin def test_bad # FIXME: this should THROW, it's badly encoded! assert_nothing_thrown { Vpim::Vcard.decode( "BEGIN:VCARD\nVERSION:3.0\nKEYencoding=b:this could be \nmy certificate\n\nEND:VCARD\n" ) } end =end def test_create card = Vpim::Vcard.create key = Vpim::DirectoryInfo.decode("key;type=x509;encoding=B:dGhpcyBjb3VsZCBiZSAKbXkgY2VydGlmaWNhdGUK\n")['key'] card << Vpim::DirectoryInfo::Field.create('key', key, 'encoding' => :b64) assert_equal(key, card['key']) #p card.to_s end def test_values # date assert_equal([2002, 4, 22], Vpim.decode_date(" 20020422 ")) assert_equal([2002, 4, 22], Vpim.decode_date(" 2002-04-22 ")) assert_equal([2002, 4, 22], Vpim.decode_date(" 2002-04-22 \n")) assert_equal([[2002, 4, 22]], Vpim.decode_date_list(" 2002-04-22 ")) assert_equal([[2002, 4, 22],[2002, 4, 22]], Vpim.decode_date_list(" 2002-04-22, 2002-04-22,")) assert_equal([[2002, 4, 22],[2002, 4, 22]], Vpim.decode_date_list(" 2002-04-22,,, , ,2002-04-22, , \n")) assert_equal([], Vpim.decode_date_list(" , , ")) # time assert_equal( [4, 53, 22, 0, nil], Vpim.decode_time(" 04:53:22 \n") ) assert_equal( [4, 53, 22, 0.10, nil], Vpim.decode_time(" 04:53:22.10 \n") ) assert_equal( [4, 53, 22, 0.10, "Z"], Vpim.decode_time(" 04:53:22.10Z \n") ) assert_equal( [4, 53, 22, 0, "Z"], Vpim.decode_time(" 045322Z \n") ) assert_equal( [4, 53, 22, 0, "+0530"], Vpim.decode_time(" 04:5322+0530 \n") ) assert_equal( [4, 53, 22, 0.10, "Z"], Vpim.decode_time(" 045322.10Z \n") ) # date-time assert_equal( [2002, 4, 22, 4, 53, 22, 0, nil], Vpim.decode_date_time("20020422T04:53:22 \n") ) assert_equal( [2002, 4, 22, 4, 53, 22, 0.10, nil], Vpim.decode_date_time(" 2002-04-22T04:53:22.10 \n") ) assert_equal( [2002, 4, 22, 4, 53, 22, 0.10, "Z"], Vpim.decode_date_time(" 20020422T04:53:22.10Z \n") ) assert_equal( [2002, 4, 22, 4, 53, 22, 0, "Z"], Vpim.decode_date_time(" 20020422T045322Z \n") ) assert_equal( [2002, 4, 22, 4, 53, 22, 0, "+0530"], Vpim.decode_date_time(" 20020422T04:5322+0530 \n") ) assert_equal( [2002, 4, 22, 4, 53, 22, 0.10, "Z"], Vpim.decode_date_time(" 20020422T045322.10Z \n") ) assert_equal( [2003, 3, 25, 3, 20, 35, 0, "Z"], Vpim.decode_date_time("20030325T032035Z") ) # text assert_equal( "aa,\n\n,\\,\\a;;b", Vpim.decode_text('aa,\\n\\n,\\\\\,\\\\a\;\;b') ) assert_equal( ['', "1\n2,3", "bbb", '', "zz", ''], Vpim.decode_text_list(',1\\n2\\,3,bbb,,zz,') ) end EX_ENCODE_1 =<<'EOF' BEGIN:VCARD VERSION:3.0 N:Roberts;Sam;;; FN:Roberts Sam EMAIL;type=HOME;type=pref:sroberts@uniserve.com TEL;type=HOME:416 535 5341 ADR;type=HOME;type=pref:;;376 Westmoreland Ave.;Toronto;ON;M6H 3A6;Canada NOTE:CATEGORIES: Amis/Famille BDAY;value=date:1970-07-14 END:VCARD EOF def test_create_1 card = Vpim::Vcard.create card << DirectoryInfo::Field.create('n', 'Roberts;Sam;;;') card << DirectoryInfo::Field.create('fn', 'Roberts Sam') card << DirectoryInfo::Field.create('email', 'sroberts@uniserve.com', 'type' => ['home', 'pref']) card << DirectoryInfo::Field.create('tel', '416 535 5341', 'type' => 'home') # TODO - allow the value to be an array, in which case it will be # concatentated with ';' card << DirectoryInfo::Field.create('adr', ';;376 Westmoreland Ave.;Toronto;ON;M6H 3A6;Canada', 'type' => ['home', 'pref']) # TODO - allow the date to be a Date, and for value to be set correctly card << DirectoryInfo::Field.create('bday', Date.new(1970, 7, 14), 'value' => 'date') # puts card.to_s end EX_BDAYS = <<'EOF' BEGIN:VCARD BDAY;value=date:206-12-15 END:VCARD BEGIN:VCARD BDAY;value=date:2003-12-09 END:VCARD BEGIN:VCARD END:VCARD EOF def test_birthday cards = Vpim::Vcard.decode(EX_BDAYS) expected = [ Date.new(Time.now.year, 12, 15), Date.new(2003, 12, 9), nil ] expected.each_with_index do | d, i| #pp d #pp i #pp cards[i] #pp cards[i].birthday.to_s #pp cards[i].birthday.class assert_equal(d, cards[i].birthday) end end EX_ATTACH=<<'---' BEGIN:VCARD VERSION:3.0 N:Middle Family;Ny_full PHOTO:val\nue PHOTO;encoding=8bit:val\nue PHOTO;encoding=8bit:val\nue PHOTO;encoding=8bit;type=atype:val\nue PHOTO;value=binary;encoding=8bit:val\nue PHOTO;value=binary;encoding=8bit:val\nue PHOTO;value=binary;encoding=8bit;type=atype:val\nue PHOTO;value=text;encoding=8bit:val\nue PHOTO;value=text;encoding=8bit:val\nue PHOTO;value=text;encoding=8bit;type=atype:val\nue PHOTO;value=uri:my:// PHOTO;value=uri;type=atype:my:// END:VCARD --- def test_attach card = Vpim::Vcard.decode(EX_ATTACH).first card.lines # FIXME - assert values are as expected end EX_21=<<'---' BEGIN:VCARD VERSION:2.1 X-EVOLUTION-FILE-AS:AAA Our Fax FN:AAA Our Fax N:AAA Our Fax ADR;WORK;PREF: LABEL;WORK;PREF: TEL;WORK;FAX:925 833-7660 TEL;HOME;FAX:925 833-7660 TEL;VOICE:1 TEL;FAX:2 EMAIL;INTERNET:e@c TITLE: NOTE: UID:pas-id-3F93E22900000001 END:VCARD --- def test_v21_modification card0 = Vpim::Vcard.decode(EX_21).first card1 = Vpim::Vcard::Maker.make2(card0) do |maker| maker.nickname = 'nickname' end card2 = Vpim::Vcard.decode(card1.encode).first assert_equal(card0.version, card1.version) assert_equal(card0.version, card2.version) end def test_v21_versioned_copy card0 = Vpim::Vcard.decode(EX_21).first card1 = Vpim::Vcard::Maker.make2(Vpim::DirectoryInfo.create([], 'VCARD')) do |maker| maker.copy card0 end card2 = Vpim::Vcard.decode(card1.encode).first assert_equal(card0.version, card2.version) end def test_v21_strip_version card0 = Vpim::Vcard.decode(EX_21).first card0.delete card0.field('VERSION') card0.delete card0.field('TEL') card0.delete card0.field('TEL') card0.delete card0.field('TEL') card0.delete card0.field('TEL') assert_raises(ArgumentError) do card0.delete card0.field('END') end assert_raises(ArgumentError) do card0.delete card0.field('BEGIN') end card1 = Vpim::Vcard::Maker.make2(Vpim::DirectoryInfo.create([], 'VCARD')) do |maker| maker.copy card0 end card2 = Vpim::Vcard.decode(card1.encode).first assert_equal(30, card2.version) assert_equal(nil, card2.field('TEL')) end EX_21_CASE0=<<'---' BEGIN:VCARD VERSION:2.1 N:Middle Family;Ny_full TEL;PREF;HOME;VOICE:0123456789 TEL;FAX:0123456789 TEL;CELL;VOICE:0123456789 TEL;HOME;VOICE:0123456789 TEL;WORK;VOICE:0123456789 EMAIL:email@email.com EMAIL:work@work.com URL:www.email.com URL:www.work.com LABEL;CHARSET=ISO-8859-1;ENCODING=QUOTED-PRINTABLE:Box 1234=0AWorkv=E4gen = 2=0AWorkv=E4gen 1=0AUme=E5=0AV=E4sterbotten=0A12345=0AS END:VCARD --- def test_v21_case0 card = Vpim::Vcard.decode(EX_21_CASE0).first # pp card.field('LABEL').value_raw # pp card.field('LABEL').value end def test_modify_name card = Vcard.decode("begin:vcard\nend:vcard\n").first assert_raises(InvalidEncodingError) do card.name end assert_raises(Unencodeable) do Vcard::Maker.make2(card) {} end card.make do |m| m.name {} end assert_equal('', card.name.given) assert_equal('', card.name.fullname) assert_raises(TypeError) do card.name.given = 'given' end card.make do |m| m.name do |n| n.given = 'given' end end assert_equal('given', card.name.given) assert_equal('given', card.name.fullname) assert_equal('' , card.name.family) card.make do |m| m.name do |n| n.family = n.given n.prefix = ' Ser ' n.fullname = 'well given' end end assert_equal('given', card.name.given) assert_equal('given', card.name.family) assert_equal('Ser given given', card.name.formatted) assert_equal('well given', card.name.fullname) end def test_add_note note = "hi\' \ \"\",,;; \n \n field" card = Vpim::Vcard::Maker.make2 do |m| m.add_note(note) m.name {} end assert_equal(note, card.note) end def test_empty_tel cin = <<___ BEGIN:VCARD TEL;HOME;FAX: END:VCARD ___ card = Vpim::Vcard.decode(cin).first assert_equal(card.telephone, nil) assert_equal(card.telephone('HOME'), nil) assert_equal([], card.telephones) end def test_slash_in_field_name cin = <<___ BEGIN:VCARD X-messaging/xmpp-All:some@jabber.id END:VCARD ___ card = Vpim::Vcard.decode(cin).first assert_equal(card.value("X-messaging/xmpp-All"), "some@jabber.id") assert_equal(card["X-messaging/xmpp-All"], "some@jabber.id") end def test_url_decode cin=<<'---' BEGIN:VCARD URL:www.email.com URL:www.work.com END:VCARD --- card = Vpim::Vcard.decode(cin).first assert_equal("www.email.com", card.url.uri) assert_equal("www.email.com", card.url.uri.to_s) assert_equal("www.email.com", card.urls.first.uri) assert_equal("www.work.com", card.urls.last.uri) end def test_bday_decode cin=<<'---' BEGIN:VCARD BDAY:1970-07-14 END:VCARD --- card = Vpim::Vcard.decode(cin).first card.birthday assert_equal(Date.new(1970, 7, 14), card.birthday) assert_equal(1, card.values("bday").size) # Nobody should have multiple bdays, I hope, but its allowed syntactically, # so test it, along with some variant forms of BDAY cin=<<'---' BEGIN:VCARD BDAY:1970-07-14 BDAY:70-7-14 BDAY:1970-07-15T03:45:12 BDAY:1970-07-15T03:45:12Z END:VCARD --- card = Vpim::Vcard.decode(cin).first assert_equal(Date.new(1970, 7, 14), card.birthday) assert_equal(4, card.values("bday").size) assert_equal(Date.new(1970, 7, 14), card.values("bday").first) assert_equal(Date.new(Time.now.year, 7, 14), card.values("bday")[1]) assert_equal(DateTime.new(1970, 7, 15, 3, 45, 12).to_s, card.values("bday")[2].to_s) assert_equal(DateTime.new(1970, 7, 15, 3, 45, 12).to_s, card.values("bday").last.to_s) end def utf_name_test(c) begin card = Vpim::Vcard.decode(c).first assert_equal("name", card.name.family) rescue $!.message << " #{c.inspect}" raise end end def be(s) s.unpack('U*').pack('n*') end def le(s) s.unpack('U*').pack('v*') end def test_utf_heuristics bom = "\xEF\xBB\xBF" dat = "BEGIN:VCARD\nN:name\nEND:VCARD\n" utf_name_test(bom+dat) utf_name_test(bom+dat.downcase) utf_name_test(dat) utf_name_test(dat.downcase) utf_name_test(be(bom+dat)) utf_name_test(be(bom+dat.downcase)) utf_name_test(be(dat)) utf_name_test(be(dat.downcase)) utf_name_test(le(bom+dat)) utf_name_test(le(bom+dat.downcase)) utf_name_test(le(dat)) utf_name_test(le(dat.downcase)) end # Broken output from Highrise. Report to support@highrisehq.com def test_highrises_invalid_google_talk_field c = <<'__' BEGIN:VCARD VERSION:3.0 REV:20080409T095515Z X-YAHOO;TYPE=HOME:yahoo.john X-GOOGLE TALK;TYPE=WORK:gtalk.john X-SAMETIME;TYPE=WORK:sametime.john X-SKYPE;TYPE=WORK:skype.john X-MSN;TYPE=WORK:msn.john X-JABBER;TYPE=WORK:jabber.john N:Doe;John;;; ADR;TYPE=WORK:;;456 Grandview Building\, Wide Street;San Diego;CA;90204; United States ADR;TYPE=HOME:;;123 Sweet Home\, Narrow Street;New York;NY;91102;United States URL;TYPE=OTHER:http\://www.homepage.com URL;TYPE=HOME:http\://www.home.com URL;TYPE=WORK:http\://www.work.com URL;TYPE=OTHER:http\://www.other.com URL;TYPE=OTHER:http\://www.custom.com ORG:John Doe & Partners Limited;; TEL;TYPE=WORK:11111111 TEL;TYPE=CELL:22222222 TEL;TYPE=HOME:33333333 TEL;TYPE=OTHER:44444444 TEL;TYPE=FAX:55555555 TEL;TYPE=FAX:66666666 TEL;TYPE=PAGER:77777777 TEL;TYPE=OTHER:88888888 TEL;TYPE=OTHER:99999999 UID:cc548e11-569e-3bf5-a9aa-722de4571f4a X-ICQ;TYPE=HOME:icq.john EMAIL;TYPE=WORK,INTERNET:john.doe@work.com EMAIL;TYPE=HOME,INTERNET:john.doe@home.com EMAIL;TYPE=OTHER,INTERNET:john.doe@other.com EMAIL;TYPE=OTHER,INTERNET:john.doe@custom.com TITLE:Sales Manager X-OTHER;TYPE=WORK:other.john X-AIM;TYPE=WORK:aim.john X-QQ;TYPE=WORK:qq.john FN:John Doe END:VCARD __ card = Vpim::Vcard.decode(c).first assert_equal("Doe", card.name.family) assert_equal("456 Grandview Building, Wide Street", card.address('work').street) assert_equal("123 Sweet Home, Narrow Street", card.address('home').street) assert_equal("John Doe & Partners Limited", card.org.first) assert_equal("gtalk.john", card.value("x-google talk")) assert_equal("http\\://www.homepage.com", card.url.uri) end def _test_gmail_vcard_export # GOOGLE BUG - Whitespace before the LABEL field values is a broken # line continuation. # GOOGLE BUG - vCards are being exported with embedded "=" in them, so # become unparseable. c = <<'__' BEGIN:VCARD VERSION:3.0 FN:Stepcase TestUser N:TestUser;Stepcase;;; EMAIL;TYPE=INTERNET:testuser@stepcase.com X-GTALK:gtalk.step X-AIM:aim.step X-YAHOO:yahoo.step X-MSN:msn.step X-ICQ:icq.step X-JABBER:jabber.step TEL;TYPE=FAX:44444444 TEL;TYPE=PAGER:66666666 TEL;TYPE=HOME:22222222 TEL;TYPE=CELL:11111111 TEL;TYPE=FAX:55555555 TEL;TYPE=WORK:33333333 LABEL;TYPE=HOME;ENCODING=QUOTED-PRINTABLE:123 Home, Home Street=0D=0A= Kowloon, N/A=0D=0A= Hong Kong LABEL;TYPE=HOME;ENCODING=QUOTED-PRINTABLE:321 Office, Work Road=0D=0A= Tsuen Wan NT=0D=0A= Hong Kong TITLE:CTO ORG:Stepcase.com NOTE:Stepcase test user is a robot. END:VCARD __ card = Vpim::Vcard.decode(c).first assert_equal("123 Home, Home Street\r\n Kowloon, N/A\r\n Hong Kong", card.value("label")) end def test_title title = "She Who Must Be Obeyed" card = Vpim::Vcard::Maker.make2 do |m| m.name do |n| n.given = "Hilda" n.family = "Rumpole" end m.title = title end assert_equal(title, card.title) card = Vpim::Vcard.decode(card.encode).first assert_equal(title, card.title) end def _test_org(*org) card = Vpim::Vcard::Maker.make2 do |m| m.name do |n| n.given = "Hilda" n.family = "Rumpole" end m.org = org end assert_equal(org, card.org) card = Vpim::Vcard.decode(card.encode).first assert_equal(org, card.org) end def test_org_single _test_org("Megamix Corp.") end def test_org_multiple _test_org("Megamix Corp.", "Marketing") end end vpim-0.695/test/test_view.rb0000644000076500000240000000311011152674120014462 0ustar samstaff#!/usr/bin/env ruby require 'test/unit' require 'vpim/repo' require 'vpim/view' class TestView < Test::Unit::TestCase View = Vpim::View Icalendar = Vpim::Icalendar def _test_week_events(vc, kind) vc = Icalendar.decode(vc.to_s.gsub("EVENT", kind)).first vv = View.week vc reader = kind.downcase + "s" kind = "check against kind=" + kind + "<\n" + vv.to_s + ">\n" assert_no_match(/yesterday/, vv.to_s, kind) assert_no_match(/nextweek/, vv.to_s, kind) assert_equal(["starts tomorrow"], vv.send(reader).map{|ve| ve.summary}, kind) end def test_week_single now = Time.now yesterday = now - View::SECSPERDAY tomorrow = now + View::SECSPERDAY nextweek = now + View::SECSPERDAY * 8 vc = Icalendar.create2 do |vc| %w{yesterday tomorrow nextweek}.each do |dtstart| vc.add_event do |ve| ve.dtstart eval(dtstart) ve.summary "starts #{dtstart}" end end end _test_week_events(vc, "EVENT") _test_week_events(vc, "TODO") _test_week_events(vc, "JOURNAL") end def test_week_recurring now = Time.now ago = now - View::SECSPERDAY * 2 vc = Icalendar.create2 do |vc| vc.add_event do |ve| ve.dtstart ago ve.dtend ago + View::SECSPERDAY / 2 ve.add_rrule do |r| r.frequency = "daily" end end end vv = View.week vc assert_equal(1, vv.events.to_a.size) ve = vv.events{|e| break e} #p ve #puts "now=" + now.to_s ve.occurrences() do |t| p [now, t, t + ve.duration] end end end