lib/ezmlm/list.rb
changeset 14 cba9fb39bcdb
parent 13 a03c08c289e9
child 15 a38e6916504c
equal deleted inserted replaced
13:a03c08c289e9 14:cba9fb39bcdb
    75 		owner = self.read( 'owner' )
    75 		owner = self.read( 'owner' )
    76 		return owner =~ /@/ ? owner : nil
    76 		return owner =~ /@/ ? owner : nil
    77 	end
    77 	end
    78 
    78 
    79 
    79 
    80 	### Return the number of messages in the list archive.
    80 	### Returns +true+ if +address+ is a subscriber to this list.
    81 	###
    81 	###
    82 	def message_count
    82 	def include?( addr, section: nil )
    83 		count = self.read( 'archnum' )
    83 		addr.downcase!
    84 		return count ? Integer( count ) : 0
    84 		file = self.subscription_dir( section ) + self.hashchar( addr )
    85 	end
    85 		return false unless file.exist?
       
    86 		return file.read.scan( /T([^\0]+)\0/ ).flatten.include?( addr )
       
    87 	end
       
    88 	alias_method :is_subscriber?, :include?
    86 
    89 
    87 
    90 
    88 	### Fetch a sorted Array of the email addresses for all of the list's
    91 	### Fetch a sorted Array of the email addresses for all of the list's
    89 	### subscribers.
    92 	### subscribers.
    90 	###
    93 	###
    91 	def subscribers
    94 	def subscribers
    92 		return self.read_subscriber_dir
    95 		return self.read_subscriber_dir
    93 	end
       
    94 
       
    95 
       
    96 	### Returns an Array of email addresses of people responsible for
       
    97 	### moderating subscription of a closed list.
       
    98 	###
       
    99 	def moderators
       
   100 		return self.read_subscriber_dir( 'mod' )
       
   101 	end
       
   102 
       
   103 
       
   104 	### Subscribe +addr+ to the list as a Moderator.
       
   105 	###
       
   106 	def add_moderator( *addr )
       
   107 		return self.subscribe( *addr, section: 'mod' )
       
   108 	end
       
   109 
       
   110 
       
   111 	### Remove +addr+ from the list as a Moderator.
       
   112 	###
       
   113 	def remove_moderator( *addr )
       
   114 		return self.unsubscribe( *addr, section: 'mod' )
       
   115 	end
       
   116 
       
   117 
       
   118 	### Returns +true+ if +address+ is a subscriber to this list.
       
   119 	###
       
   120 	def include?( addr )
       
   121 		addr.downcase!
       
   122 		file = self.subscription_dir + self.hashchar( addr )
       
   123 		return false unless file.exist?
       
   124 		return file.read.scan( /T([^\0]+)\0/ ).flatten.include?( addr )
       
   125 	end
    96 	end
   126 
    97 
   127 
    98 
   128 	### Subscribe +addr+ to the list within +section+.
    99 	### Subscribe +addr+ to the list within +section+.
   129 	###
   100 	###
   147 					end
   118 					end
   148 				end
   119 				end
   149 			end
   120 			end
   150 		end
   121 		end
   151 	end
   122 	end
       
   123 	alias_method :add_subscriber, :subscribe
   152 
   124 
   153 
   125 
   154 	### Unsubscribe +addr+ from the list within +section+.
   126 	### Unsubscribe +addr+ from the list within +section+.
   155 	###
   127 	###
   156 	def unsubscribe( *addr, section: nil )
   128 	def unsubscribe( *addr, section: nil )
   157 		addr.each do |address|
   129 		addr.each do |address|
   158 			address.downcase!
   130 			address.downcase!
   159 
   131 
   160 			file = self.subscribers_dir( section ) + self.hashchar( address )
   132 			file = self.subscription_dir( section ) + self.hashchar( address )
   161 			self.with_safety do
   133 			self.with_safety do
   162 				next unless file.exist?
   134 				next unless file.exist?
   163 				addresses = file.read.scan( /T([^\0]+)\0/ ).flatten
   135 				addresses = file.read.scan( /T([^\0]+)\0/ ).flatten
   164 				addresses = addresses - [ address ]
   136 				addresses = addresses - [ address ]
   165 
   137 
   171 					end
   143 					end
   172 				end
   144 				end
   173 			end
   145 			end
   174 		end
   146 		end
   175 	end
   147 	end
   176 
   148 	alias_method :remove_subscriber, :unsubscribe
   177 
   149 
   178 =begin
   150 
   179 	### Return the Date parsed from the last post to the list.
   151 	### Returns an Array of email addresses of people responsible for
   180 	###
   152 	### moderating subscription of a closed list.
   181 	def last_message_date
   153 	###
   182 		mail = self.last_post or return nil
   154 	def moderators
   183 		return mail.date
   155 		return self.read_subscriber_dir( 'mod' )
   184 	end
   156 	end
   185 
   157 
   186 
   158 	### Returns +true+ if +address+ is a moderator.
   187 	### Return the author of the last post to the list.
   159 	###
   188 	###
   160 	def is_moderator?( addr )
   189 	def last_message_author
   161 		return self.include?( addr, section: 'mod' )
   190 		mail = self.last_post or return nil
   162 	end
   191 		return mail.from
   163 
   192 	end
   164 	### Subscribe +addr+ to the list as a Moderator.
   193 
   165 	###
   194 
   166 	def add_moderator( *addr )
   195 	### Returns +true+ if subscription to the list is moderated.
   167 		return self.subscribe( *addr, section: 'mod' )
   196 	###
   168 	end
   197 	def closed?
   169 
   198 		return (self.listdir + 'modsub').exist? || (self.listdir + 'remote').exist?
   170 	### Remove +addr+ from the list as a Moderator.
   199 	end
   171 	###
   200 
   172 	def remove_moderator( *addr )
   201 
   173 		return self.unsubscribe( *addr, section: 'mod' )
   202 	### Returns +true+ if posting to the list is moderated.
   174 	end
       
   175 
       
   176 
       
   177 	### Returns an Array of email addresses denied access
       
   178 	### to the list.
       
   179 	###
       
   180 	def blacklisted
       
   181 		return self.read_subscriber_dir( 'deny' )
       
   182 	end
       
   183 
       
   184 	### Returns +true+ if +address+ is disallowed from participating.
       
   185 	###
       
   186 	def is_blacklisted?( addr )
       
   187 		return self.include?( addr, section: 'deny' )
       
   188 	end
       
   189 
       
   190 	### Blacklist +addr+ from the list.
       
   191 	###
       
   192 	def add_blacklisted( *addr )
       
   193 		return self.subscribe( *addr, section: 'deny' )
       
   194 	end
       
   195 
       
   196 	### Remove +addr+ from the blacklist.
       
   197 	###
       
   198 	def remove_blacklisted( *addr )
       
   199 		return self.unsubscribe( *addr, section: 'deny' )
       
   200 	end
       
   201 
       
   202 
       
   203 
       
   204 	### Returns an Array of email addresses that act like
       
   205 	### regular subscribers for user-post only lists.
       
   206 	###
       
   207 	def allowed
       
   208 		return self.read_subscriber_dir( 'allow' )
       
   209 	end
       
   210 
       
   211 	### Returns +true+ if +address+ is given the same benefits as a
       
   212 	### regular subscriber for user-post only lists.
       
   213 	###
       
   214 	def is_allowed?( addr )
       
   215 		return self.include?( addr, section: 'allow' )
       
   216 	end
       
   217 
       
   218 	### Add +addr+ to allow posting to user-post only lists,
       
   219 	### when +addr+ isn't a subscriber.
       
   220 	###
       
   221 	def add_allowed( *addr )
       
   222 		return self.subscribe( *addr, section: 'allow' )
       
   223 	end
       
   224 
       
   225 	### Remove +addr+ from the allowed list.
       
   226 	###
       
   227 	def remove_allowed( *addr )
       
   228 		return self.unsubscribe( *addr, section: 'allow' )
       
   229 	end
       
   230 
       
   231 
       
   232 	### Returns +true+ if message threading is enabled.
       
   233 	###
       
   234 	def threaded?
       
   235 		return ( self.listdir + 'threaded' ).exist?
       
   236 	end
       
   237 
       
   238 	### Disable or enable message threading.
       
   239 	###
       
   240 	### This automatically builds message indexes and thread
       
   241 	### information on an incoming message.
       
   242 	###
       
   243 	def threaded=( enable=true )
       
   244 		if enable
       
   245 			self.touch( 'threaded' )
       
   246 		else
       
   247 			self.unlink( 'threaded' )
       
   248 		end
       
   249 	end
       
   250 
       
   251 
       
   252 	### Returns +true+ if the list is configured to respond
       
   253 	### to remote mangement requests.
       
   254 	###
       
   255 	def public?
       
   256 		return ( self.listdir + 'public' ).exist?
       
   257 	end
       
   258 
       
   259 	### Disable or enable remote management requests.
       
   260 	###
       
   261 	def public=( enable=true )
       
   262 		if enable
       
   263 			self.touch( 'public' )
       
   264 		else
       
   265 			self.unlink( 'public' )
       
   266 		end
       
   267 	end
       
   268 
       
   269 	### Returns +true+ if the list is not configured to respond
       
   270 	### to remote mangement requests.
       
   271 	###
       
   272 	def private?
       
   273 		return ! self.public?
       
   274 	end
       
   275 
       
   276 	### Disable or enable remote management requests.
       
   277 	###
       
   278 	def private=( enable=false )
       
   279 		self.public = ! enable
       
   280 	end
       
   281 
       
   282 
       
   283 	### Returns +true+ if the list supports remote administration
       
   284 	### subscribe/unsubscribe requests from moderators.
       
   285 	###
       
   286 	def remote_subscriptions?
       
   287 		return ( self.listdir + 'remote' ).exist?
       
   288 	end
       
   289 
       
   290 	### Disable or enable remote subscription requests.
       
   291 	###
       
   292 	def remote_subscriptions=( enable=false )
       
   293 		if enable
       
   294 			self.touch( 'remote' )
       
   295 		else
       
   296 			self.unlink( 'remote' )
       
   297 		end
       
   298 	end
       
   299 
       
   300 
       
   301 	### Returns +true+ if list subscription requests require moderator
       
   302 	### approval.
       
   303 	###
       
   304 	def moderated_subscriptions?
       
   305 		return ( self.listdir + 'modsub' ).exist?
       
   306 	end
       
   307 
       
   308 	### Disable or enable subscription moderation.
       
   309 	###
       
   310 	def moderated_subscriptions=( enable=false )
       
   311 		if enable
       
   312 			self.touch( 'modsub' )
       
   313 		else
       
   314 			self.unlink( 'modsub' )
       
   315 		end
       
   316 	end
       
   317 
       
   318 
       
   319 	### Returns +true+ if message moderation is enabled.
   203 	###
   320 	###
   204 	def moderated?
   321 	def moderated?
   205 		return (self.listdir + 'modpost').exist?
   322 		return ( self.listdir + 'modpost' ).exist?
   206 	end
   323 	end
   207 
   324 
   208 
   325 	### Disable or enable message moderation.
   209 	### Return a Mail::Message object loaded from the last post to the list. Returns
   326 	###
   210 	### +nil+ if there are no archived posts.
   327 	### This has special meaning when combined with user_post_only setting.
       
   328 	### Lists act as unmoderated for subscribers, and posts from unknown
       
   329 	### addresses go to moderation.
       
   330 	###
       
   331 	def moderated=( enable=false )
       
   332 		if enable
       
   333 			self.touch( 'modpost' )
       
   334 			self.touch( 'noreturnposts' ) if self.user_posts_only?
       
   335 		else
       
   336 			self.unlink( 'modpost' )
       
   337 			self.unlink( 'noreturnposts' ) if self.user_posts_only?
       
   338 		end
       
   339 	end
       
   340 
       
   341 
       
   342 	### Returns +true+ if posting is only allowed by moderators.
       
   343 	###
       
   344 	def moderator_posts_only?
       
   345 		return ( self.listdir + 'modpostonly' ).exist?
       
   346 	end
       
   347 
       
   348 	### Disable or enable moderation only posts.
       
   349 	###
       
   350 	def moderator_posts_only=( enable=false )
       
   351 		if enable
       
   352 			self.touch( 'modpostonly' )
       
   353 		else
       
   354 			self.unlink( 'modpostonly' )
       
   355 		end
       
   356 	end
       
   357 
       
   358 
       
   359 	### Returns +true+ if posting is only allowed by subscribers.
       
   360 	###
       
   361 	def user_posts_only?
       
   362 		return ( self.listdir + 'subpostonly' ).exist?
       
   363 	end
       
   364 
       
   365 	### Disable or enable user only posts.
       
   366 	### This is easily defeated, moderated lists are preferred.
       
   367 	###
       
   368 	### This has special meaning for moderated lists.  Lists act as
       
   369 	### unmoderated for subscribers, and posts from unknown addresses
       
   370 	### go to moderation.
       
   371 	###
       
   372 	def user_posts_only=( enable=false )
       
   373 		if enable
       
   374 			self.touch( 'subpostonly' )
       
   375 			self.touch( 'noreturnposts' )if self.moderated?
       
   376 		else
       
   377 			self.unlink( 'subpostonly' )
       
   378 			self.unlink( 'noreturnposts' ) if self.moderated?
       
   379 		end
       
   380 	end
       
   381 
       
   382 
       
   383 
       
   384 	### Returns +true+ if message archival is enabled.
       
   385 	###
       
   386 	def archived?
       
   387 		return ( self.listdir + 'archived' ).exist? || ( self.listdir + 'indexed' ).exist?
       
   388 	end
       
   389 
       
   390 	### Disable or enable message archiving (and indexing.)
       
   391 	###
       
   392 	def archive=( enable=true )
       
   393 		if enable
       
   394 			self.touch( 'archived' )
       
   395 			self.touch( 'indexed' )
       
   396 		else
       
   397 			self.unlink( 'archived' )
       
   398 			self.unlink( 'indexed' )
       
   399 		end
       
   400 	end
       
   401 
       
   402 	### Returns +true+ if the message archive is accessible only to
       
   403 	### moderators.
       
   404 	###
       
   405 	def private_archive?
       
   406 		return ( self.listdir + 'modgetonly' ).exist?
       
   407 	end
       
   408 
       
   409 	### Disable or enable private access to the archive.
       
   410 	###
       
   411 	def private_archive=( enable=true )
       
   412 		if enable
       
   413 			self.touch( 'modgetonly' )
       
   414 		else
       
   415 			self.unlink( 'modgetonly' )
       
   416 		end
       
   417 	end
       
   418 
       
   419 	### Returns +true+ if the message archive is accessible to anyone.
       
   420 	###
       
   421 	def public_archive?
       
   422 		return ! self.private_archive?
       
   423 	end
       
   424 
       
   425 	### Returns +true+ if the message archive is accessible only to
       
   426 	### list subscribers.
       
   427 	###
       
   428 	def guarded_archive?
       
   429 		return ( self.listdir + 'subgetonly' ).exist?
       
   430 	end
       
   431 
       
   432 	### Disable or enable loimited access to the archive.
       
   433 	###
       
   434 	def guarded_archive=( enable=true )
       
   435 		if enable
       
   436 			self.touch( 'subgetonly' )
       
   437 		else
       
   438 			self.unlink( 'subgetonly' )
       
   439 		end
       
   440 	end
       
   441 
       
   442 
       
   443 	### Returns +true+ if message digests are enabled.
       
   444 	###
       
   445 	def digested?
       
   446 		return ( self.listdir + 'digested' ).exist?
       
   447 	end
       
   448 
       
   449 	### Disable or enable message digesting.
       
   450 	###
       
   451 	def digest=( enable=true )
       
   452 		if enable
       
   453 			self.touch( 'digested' )
       
   454 		else
       
   455 			self.unlink( 'digested' )
       
   456 		end
       
   457 	end
       
   458 
       
   459 	### If the list is digestable, trigger the digest after this amount
       
   460 	### of message body since the latest digest, in kbytes.
       
   461 	###
       
   462 	### See: ezmlm-tstdig(1)
       
   463 	###
       
   464 	def digest_kbytesize
       
   465 		size = self.read( 'digsize' ).to_i
       
   466 		return size.zero? ? 64 : size
       
   467 	end
       
   468 
       
   469 	### If the list is digestable, trigger the digest after this amount
       
   470 	### of message body since the latest digest, in kbytes.
       
   471 	###
       
   472 	### See: ezmlm-tstdig(1)
       
   473 	###
       
   474 	def digest_kbytesize=( size=64 )
       
   475 		self.write( 'digsize' ) {|f| f.puts size.to_i }
       
   476 	end
       
   477 
       
   478 	### If the list is digestable, trigger the digest after this many
       
   479 	### messages have accumulated since the latest digest.
       
   480 	###
       
   481 	### See: ezmlm-tstdig(1)
       
   482 	###
       
   483 	def digest_count
       
   484 		count = self.read( 'digcount' ).to_i
       
   485 		return count.zero? ? 30 : count
       
   486 	end
       
   487 
       
   488 	### If the list is digestable, trigger the digest after this many
       
   489 	### messages have accumulated since the latest digest.
       
   490 	###
       
   491 	### See: ezmlm-tstdig(1)
       
   492 	###
       
   493 	def digest_count=( count=30 )
       
   494 		self.write( 'digcount' ) {|f| f.puts count.to_i }
       
   495 	end
       
   496 
       
   497 	### If the list is digestable, trigger the digest after this much
       
   498 	### time has passed since the last digest, in hours.
       
   499 	###
       
   500 	### See: ezmlm-tstdig(1)
       
   501 	###
       
   502 	def digest_timeout
       
   503 		hours = self.read( 'digtime' ).to_i
       
   504 		return hours.zero? ? 48 : hours
       
   505 	end
       
   506 
       
   507 	### If the list is digestable, trigger the digest after this much
       
   508 	### time has passed since the last digest, in hours.
       
   509 	###
       
   510 	### See: ezmlm-tstdig(1)
       
   511 	###
       
   512 	def digest_timeout=( hours=48 )
       
   513 		self.write( 'digtime' ) {|f| f.puts hours.to_i }
       
   514 	end
       
   515 
       
   516 
       
   517 	### Returns +true+ if the list requires subscriptions to be
       
   518 	### confirmed.  AKA "help" mode if disabled.
       
   519 	###
       
   520 	def confirm_subscriptions?
       
   521 		return ! ( self.listdir + 'nosubconfirm' ).exist?
       
   522 	end
       
   523 
       
   524 	### Disable or enable subscription confirmation.
       
   525 	### AKA "help" mode if disabled.
       
   526 	###
       
   527 	def confirm_subscriptions=( enable=true )
       
   528 		if enable
       
   529 			self.unlink( 'nosubconfirm' )
       
   530 		else
       
   531 			self.touch( 'nosubconfirm' )
       
   532 		end
       
   533 	end
       
   534 
       
   535 	### Returns +true+ if the list requires unsubscriptions to be
       
   536 	### confirmed.  AKA "jump" mode.
       
   537 	###
       
   538 	def confirm_unsubscriptions?
       
   539 		return ! ( self.listdir + 'nounsubconfirm' ).exist?
       
   540 	end
       
   541 
       
   542 	### Disable or enable unsubscription confirmation.
       
   543 	### AKA "jump" mode.
       
   544 	###
       
   545 	def confirm_unsubscriptions=( enable=true )
       
   546 		if enable
       
   547 			self.unlink( 'nounsubconfirm' )
       
   548 		else
       
   549 			self.touch( 'nounsubconfirm' )
       
   550 		end
       
   551 	end
       
   552 
       
   553 
       
   554 	### Returns +true+ if the list requires regular message postings
       
   555 	### to be confirmed by the original sender.
       
   556 	###
       
   557 	def confirm_postings?
       
   558 		return ( self.listdir + 'confirmpost' ).exist?
       
   559 	end
       
   560 
       
   561 	### Disable or enable message confirmation.
       
   562 	###
       
   563 	def confirm_postings=( enable=false )
       
   564 		if enable
       
   565 			self.touch( 'confirmpost' )
       
   566 		else
       
   567 			self.unlink( 'confirmpost' )
       
   568 		end
       
   569 	end
       
   570 
       
   571 
       
   572 	### Returns +true+ if the list allows moderators to
       
   573 	### fetch a subscriber list remotely.
       
   574 	###
       
   575 	def allow_remote_listing?
       
   576 		return ( self.listdir + 'modcanlist' ).exist?
       
   577 	end
       
   578 
       
   579 	### Disable or enable the ability for moderators to
       
   580 	### remotely fetch a subscriber list.
       
   581 	###
       
   582 	def allow_remote_listing=( enable=false )
       
   583 		if enable
       
   584 			self.touch( 'modcanlist' )
       
   585 		else
       
   586 			self.unlink( 'modcanlist' )
       
   587 		end
       
   588 	end
       
   589 
       
   590 
       
   591 	### Returns +true+ if the list automatically manages
       
   592 	### bouncing subscriber addresses.
       
   593 	###
       
   594 	def bounce_warnings?
       
   595 		return ! ( self.listdir + 'nowarn' ).exist?
       
   596 	end
       
   597 
       
   598 	### Disable or enable automatic bounce probes and warnings.
       
   599 	###
       
   600 	def bounce_warnings=( enable=true )
       
   601 		if enable
       
   602 			self.unlink( 'nowarn' )
       
   603 		else
       
   604 			self.touch( 'nowarn' )
       
   605 		end
       
   606 	end
       
   607 
       
   608 
       
   609 	### Return the maximum message size, in bytes.  Messages larger than
       
   610 	### this size will be rejected.
       
   611 	###
       
   612 	### See: ezmlm-reject(1)
       
   613 	###
       
   614 	def maximum_message_size
       
   615 		size = self.read( 'msgsize' )
       
   616 		return size ? size.split( ':' ).first.to_i : 0
       
   617 	end
       
   618 
       
   619 	### Set the maximum message size, in bytes.  Messages larger than
       
   620 	### this size will be rejected.
       
   621 	###
       
   622 	### See: ezmlm-reject(1)
       
   623 	###
       
   624 	def maximum_message_size=( size=307200 )
       
   625 		if size.to_i.zero?
       
   626 			self.unlink( 'msgsize' )
       
   627 		else
       
   628 			self.write( 'msgsize' ) {|f| f.puts "#{size.to_i}:0" }
       
   629 		end
       
   630 	end
       
   631 
       
   632 
       
   633 	### Return the number of messages in the list archive.
       
   634 	###
       
   635 	def message_count
       
   636 		count = self.read( 'archnum' )
       
   637 		return count ? Integer( count ) : 0
       
   638 	end
       
   639 
       
   640 	### Returns the last message to the list as a Mail::Message, if
       
   641 	### archiving was enabled.
   211 	###
   642 	###
   212 	def last_post
   643 	def last_post
   213 		archivedir = self.listdir + 'archive'
   644 		num = self.message_count
   214 		return nil unless archivedir.exist?
   645 		return if num.zero?
   215 
   646 
   216 		# Find the last numbered directory under the archive dir
   647 		hashdir = num / 100
   217 		last_archdir = Pathname.glob( archivedir + '[0-9]*' ).
   648 		message = "%02d" % [ num % 100 ]
   218 			sort_by {|pn| Integer(pn.basename.to_s) }.last
   649 
   219 
   650 		post = self.listdir + 'archive' + hashdir.to_s + message.to_s
   220 		return nil unless last_archdir
   651 		return unless post.exist?
   221 
   652 
   222 		# Find the last numbered file under the last numbered directory we found
   653 		return Mail.read( post.to_s )
   223 		# above.
   654 	end
   224 		last_post_path = Pathname.glob( last_archdir + '[0-9]*' ).
       
   225 			sort_by {|pn| pn.basename.to_s }.last
       
   226 
       
   227 		raise RuntimeError, "unexpectedly empty archive directory '%s'" % [ last_archdir ] \
       
   228 			unless last_post_path
       
   229 
       
   230 				require 'pry'
       
   231 				binding.pry
       
   232 		last_post = TMail::Mail.load( last_post_path.to_s )
       
   233 	end
       
   234 =end
       
   235 
   655 
   236 
   656 
   237 	#########
   657 	#########
   238 	protected
   658 	protected
   239 	#########
   659 	#########
   242 	### fast user lookups.  Returns the hashed integer.
   662 	### fast user lookups.  Returns the hashed integer.
   243 	###
   663 	###
   244 	### Older ezmlm didn't lowercase addresses, anything within the last
   664 	### Older ezmlm didn't lowercase addresses, anything within the last
   245 	### decade did.  We're not going to worry about compatibility there.
   665 	### decade did.  We're not going to worry about compatibility there.
   246 	###
   666 	###
   247 	### (See subhash.c in the ezmlm source.)
   667 	### See: subhash.c in the ezmlm source.
   248 	###
   668 	###
   249 	def subhash( addr )
   669 	def subhash( addr )
   250 		h = 5381
   670 		h = 5381
   251 		over = 2 ** ADDRESS_SPACE
   671 		over = 2 ** ADDRESS_SPACE
   252 
   672 
   275 	rescue
   695 	rescue
   276 		nil
   696 		nil
   277 	end
   697 	end
   278 
   698 
   279 
   699 
       
   700 	### Overwrite +file+ safely, yielding the open filehandle to the
       
   701 	### block.  Set the new file to correct ownership and permissions.
       
   702 	###
       
   703 	def write( file, &block )
       
   704 		file = self.listdir + file unless file.is_a?( Pathname )
       
   705 		self.with_safety do
       
   706 			file.open( 'w' ) do |f|
       
   707 				yield( f )
       
   708 			end
       
   709 
       
   710 			stat = self.listdir.stat
       
   711 			file.chown( stat.uid, stat.gid )
       
   712 			file.chmod( 0600 )
       
   713 		end
       
   714 	end
       
   715 
       
   716 
       
   717 	### Simply create an empty file, safely.
       
   718 	###
       
   719 	def touch( file )
       
   720 		self.write( file ) {}
       
   721 	end
       
   722 
       
   723 
       
   724 	### Delete +file+ safely.
       
   725 	###
       
   726 	def unlink( file )
       
   727 		file = self.listdir + file unless file.is_a?( Pathname )
       
   728 		return unless file.exist?
       
   729 		self.with_safety do
       
   730 			file.unlink
       
   731 		end
       
   732 	end
       
   733 
       
   734 
   280 	### Return a Pathname to a subscription directory.
   735 	### Return a Pathname to a subscription directory.
   281 	###
   736 	###
   282 	def subscription_dir( section=nil )
   737 	def subscription_dir( section=nil )
   283 		section = nil if section && ! SUBSCRIPTION_DIRS.include?( section )
       
   284 
       
   285 		if section
   738 		if section
       
   739 			unless SUBSCRIPTION_DIRS.include?( section )
       
   740 				raise "Invalid subscription dir: %s, must be one of: %s" % [
       
   741 					section,
       
   742 					SUBSCRIPTION_DIRS.join( ', ' )
       
   743 				]
       
   744 			end
   286 			return self.listdir + section + 'subscribers'
   745 			return self.listdir + section + 'subscribers'
   287 		else
   746 		else
   288 			return self.listdir + 'subscribers'
   747 			return self.listdir + 'subscribers'
   289 		end
   748 		end
   290 	end
   749 	end
   291 
   750 
   292 
   751 
   293 	### Read the hashed subscriber email addresses from the specified +directory+ and return them in
   752 	### Read the hashed subscriber email addresses from the specified
   294 	### an Array.
   753 	### +directory+ and return them in an Array.
   295 	###
   754 	###
   296 	def read_subscriber_dir( section=nil )
   755 	def read_subscriber_dir( section=nil )
   297 		directory = self.subscription_dir( section )
   756 		directory = self.subscription_dir( section )
   298 		rval = []
   757 		rval = []
   299 		Pathname.glob( directory + '*' ) do |hashfile|
   758 		Pathname.glob( directory + '*' ) do |hashfile|