don't send when password is supplied, RT#34705
[freeside.git] / FS / FS / contact.pm
1 package FS::contact;
2 use base qw( FS::Record );
3
4 use strict;
5 use vars qw( $skip_fuzzyfiles );
6 use Carp;
7 use Scalar::Util qw( blessed );
8 use FS::Record qw( qsearch qsearchs dbh );
9 use FS::contact_phone;
10 use FS::contact_email;
11 use FS::queue;
12 use FS::phone_type; #for cgi_contact_fields
13 use FS::cust_contact;
14 use FS::prospect_contact;
15
16 $skip_fuzzyfiles = 0;
17
18 =head1 NAME
19
20 FS::contact - Object methods for contact records
21
22 =head1 SYNOPSIS
23
24   use FS::contact;
25
26   $record = new FS::contact \%hash;
27   $record = new FS::contact { 'column' => 'value' };
28
29   $error = $record->insert;
30
31   $error = $new_record->replace($old_record);
32
33   $error = $record->delete;
34
35   $error = $record->check;
36
37 =head1 DESCRIPTION
38
39 An FS::contact object represents an specific contact person for a prospect or
40 customer.  FS::contact inherits from FS::Record.  The following fields are
41 currently supported:
42
43 =over 4
44
45 =item contactnum
46
47 primary key
48
49 =item prospectnum
50
51 prospectnum
52
53 =item custnum
54
55 custnum
56
57 =item locationnum
58
59 locationnum
60
61 =item last
62
63 last
64
65 =item first
66
67 first
68
69 =item title
70
71 title
72
73 =item comment
74
75 comment
76
77 =item selfservice_access
78
79 empty or Y
80
81 =item _password
82
83 =item _password_encoding
84
85 empty or bcrypt
86
87 =item disabled
88
89 disabled
90
91
92 =back
93
94 =head1 METHODS
95
96 =over 4
97
98 =item new HASHREF
99
100 Creates a new contact.  To add the contact to the database, see L<"insert">.
101
102 Note that this stores the hash reference, not a distinct copy of the hash it
103 points to.  You can ask the object for a copy with the I<hash> method.
104
105 =cut
106
107 sub table { 'contact'; }
108
109 =item insert
110
111 Adds this record to the database.  If there is an error, returns the error,
112 otherwise returns false.
113
114 =cut
115
116 sub insert {
117   my $self = shift;
118
119   local $SIG{INT} = 'IGNORE';
120   local $SIG{QUIT} = 'IGNORE';
121   local $SIG{TERM} = 'IGNORE';
122   local $SIG{TSTP} = 'IGNORE';
123   local $SIG{PIPE} = 'IGNORE';
124
125   my $oldAutoCommit = $FS::UID::AutoCommit;
126   local $FS::UID::AutoCommit = 0;
127   my $dbh = dbh;
128
129   #save off and blank values that move to cust_contact / prospect_contact now
130   my $prospectnum = $self->prospectnum;
131   $self->prospectnum('');
132   my $custnum = $self->custnum;
133   $self->custnum('');
134
135   my %link_hash = ();
136   for (qw( classnum comment selfservice_access )) {
137     $link_hash{$_} = $self->get($_);
138     $self->$_('');
139   }
140
141   #look for an existing contact with this email address
142   my $existing_contact = '';
143   if ( $self->get('emailaddress') =~ /\S/ ) {
144   
145     my %existing_contact = ();
146
147     foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
148  
149       my $contact_email = qsearchs('contact_email', { emailaddress=>$email } )
150         or next;
151
152       my $contact = $contact_email->contact;
153       $existing_contact{ $contact->contactnum } = $contact;
154
155     }
156
157     if ( scalar( keys %existing_contact ) > 1 ) {
158       $dbh->rollback if $oldAutoCommit;
159       return 'Multiple email addresses specified '.
160              ' that already belong to separate contacts';
161     } elsif ( scalar( keys %existing_contact ) ) {
162       ($existing_contact) = values %existing_contact;
163     }
164
165   }
166
167   if ( $existing_contact ) {
168
169     $self->$_($existing_contact->$_())
170       for qw( contactnum _password _password_encoding );
171     $self->SUPER::replace($existing_contact);
172
173   } else {
174
175     my $error = $self->SUPER::insert;
176     if ( $error ) {
177       $dbh->rollback if $oldAutoCommit;
178       return $error;
179     }
180
181   }
182
183   my $cust_contact = '';
184   if ( $custnum ) {
185     my %hash = ( 'contactnum' => $self->contactnum,
186                  'custnum'    => $custnum,
187                );
188     $cust_contact =  qsearchs('cust_contact', \%hash )
189                   || new FS::cust_contact { %hash, %link_hash };
190     my $error = $cust_contact->custcontactnum ? $cust_contact->replace
191                                               : $cust_contact->insert;
192     if ( $error ) {
193       $dbh->rollback if $oldAutoCommit;
194       return $error;
195     }
196   }
197
198   if ( $prospectnum ) {
199     my %hash = ( 'contactnum'  => $self->contactnum,
200                  'prospectnum' => $prospectnum,
201                );
202     my $prospect_contact =  qsearchs('prospect_contact', \%hash )
203                          || new FS::prospect_contact { %hash, %link_hash };
204     my $error =
205       $prospect_contact->prospectcontactnum ? $prospect_contact->replace
206                                             : $prospect_contact->insert;
207     if ( $error ) {
208       $dbh->rollback if $oldAutoCommit;
209       return $error;
210     }
211   }
212
213   foreach my $pf ( grep { /^phonetypenum(\d+)$/ && $self->get($_) =~ /\S/ }
214                         keys %{ $self->hashref } ) {
215     $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
216     my $phonetypenum = $1;
217
218     my %hash = ( 'contactnum'   => $self->contactnum,
219                  'phonetypenum' => $phonetypenum,
220                );
221     my $contact_phone =
222       qsearchs('contact_phone', \%hash)
223         || new FS::contact_phone { %hash, _parse_phonestring($self->get($pf)) };
224     my $error = $contact_phone->contactphonenum ? $contact_phone->replace
225                                                 : $contact_phone->insert;
226     if ( $error ) {
227       $dbh->rollback if $oldAutoCommit;
228       return $error;
229     }
230   }
231
232   if ( $self->get('emailaddress') =~ /\S/ ) {
233
234     foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
235       my %hash = (
236         'contactnum'   => $self->contactnum,
237         'emailaddress' => $email,
238       );
239       unless ( qsearchs('contact_email', \%hash) ) {
240         my $contact_email = new FS::contact_email \%hash;
241         my $error = $contact_email->insert;
242         if ( $error ) {
243           $dbh->rollback if $oldAutoCommit;
244           return $error;
245         }
246       }
247     }
248
249   }
250
251   unless ( $skip_fuzzyfiles ) { #unless ( $import || $skip_fuzzyfiles ) {
252     #warn "  queueing fuzzyfiles update\n"
253     #  if $DEBUG > 1;
254     my $error = $self->queue_fuzzyfiles_update;
255     if ( $error ) {
256       $dbh->rollback if $oldAutoCommit;
257       return "updating fuzzy search cache: $error";
258     }
259   }
260
261   if (      $link_hash{'selfservice_access'} eq 'R'
262        or ( $link_hash{'selfservice_access'}
263             && $cust_contact
264             && ! length($self->_password)
265           )
266      )
267   {
268     my $error = $self->send_reset_email( queue=>1 );
269     if ( $error ) {
270       $dbh->rollback if $oldAutoCommit;
271       return $error;
272     }
273   }
274
275   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
276
277   '';
278
279 }
280
281 =item delete
282
283 Delete this record from the database.
284
285 =cut
286
287 sub delete {
288   my $self = shift;
289
290   local $SIG{HUP} = 'IGNORE';
291   local $SIG{INT} = 'IGNORE';
292   local $SIG{QUIT} = 'IGNORE';
293   local $SIG{TERM} = 'IGNORE';
294   local $SIG{TSTP} = 'IGNORE';
295   local $SIG{PIPE} = 'IGNORE';
296
297   my $oldAutoCommit = $FS::UID::AutoCommit;
298   local $FS::UID::AutoCommit = 0;
299   my $dbh = dbh;
300
301   #got a prospetnum or custnum? delete the prospect_contact or cust_contact link
302
303   if ( $self->prospectnum ) {
304     my $prospect_contact = qsearchs('prospect_contact', {
305                              'contactnum'  => $self->contactnum,
306                              'prospectnum' => $self->prospectnum,
307                            });
308     my $error = $prospect_contact->delete;
309     if ( $error ) {
310       $dbh->rollback if $oldAutoCommit;
311       return $error;
312     }
313   }
314
315   if ( $self->custnum ) {
316     my $cust_contact = qsearchs('cust_contact', {
317                          'contactnum'  => $self->contactnum,
318                          'custnum' => $self->custnum,
319                        });
320     my $error = $cust_contact->delete;
321     if ( $error ) {
322       $dbh->rollback if $oldAutoCommit;
323       return $error;
324     }
325   }
326
327   # then, proceed with deletion only if the contact isn't attached to any other
328   # prospects or customers
329
330   #inefficient, but how many prospects/customers can a single contact be
331   # attached too?  (and is removing them from one a common operation?)
332   if ( $self->prospect_contact || $self->cust_contact ) {
333     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
334     return '';
335   }
336
337   #proceed with deletion
338
339   foreach my $cust_pkg ( $self->cust_pkg ) {
340     $cust_pkg->contactnum('');
341     my $error = $cust_pkg->replace;
342     if ( $error ) {
343       $dbh->rollback if $oldAutoCommit;
344       return $error;
345     }
346   }
347
348   foreach my $object ( $self->contact_phone, $self->contact_email ) {
349     my $error = $object->delete;
350     if ( $error ) {
351       $dbh->rollback if $oldAutoCommit;
352       return $error;
353     }
354   }
355
356   my $error = $self->SUPER::delete;
357   if ( $error ) {
358     $dbh->rollback if $oldAutoCommit;
359     return $error;
360   }
361
362   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
363   '';
364
365 }
366
367 =item replace OLD_RECORD
368
369 Replaces the OLD_RECORD with this one in the database.  If there is an error,
370 returns the error, otherwise returns false.
371
372 =cut
373
374 sub replace {
375   my $self = shift;
376
377   my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
378               ? shift
379               : $self->replace_old;
380
381   $self->$_( $self->$_ || $old->$_ ) for qw( _password _password_encoding );
382
383   local $SIG{INT} = 'IGNORE';
384   local $SIG{QUIT} = 'IGNORE';
385   local $SIG{TERM} = 'IGNORE';
386   local $SIG{TSTP} = 'IGNORE';
387   local $SIG{PIPE} = 'IGNORE';
388
389   my $oldAutoCommit = $FS::UID::AutoCommit;
390   local $FS::UID::AutoCommit = 0;
391   my $dbh = dbh;
392
393   #save off and blank values that move to cust_contact / prospect_contact now
394   my $prospectnum = $self->prospectnum;
395   $self->prospectnum('');
396   my $custnum = $self->custnum;
397   $self->custnum('');
398
399   my %link_hash = ();
400   for (qw( classnum comment selfservice_access )) {
401     $link_hash{$_} = $self->get($_);
402     $self->$_('');
403   }
404
405   my $error = $self->SUPER::replace($old);
406   if ( $error ) {
407     $dbh->rollback if $oldAutoCommit;
408     return $error;
409   }
410
411   my $cust_contact = '';
412   if ( $custnum ) {
413     my %hash = ( 'contactnum' => $self->contactnum,
414                  'custnum'    => $custnum,
415                );
416     my $error;
417     if ( $cust_contact = qsearchs('cust_contact', \%hash ) ) {
418       $cust_contact->$_($link_hash{$_}) for keys %link_hash;
419       $error = $cust_contact->replace;
420     } else {
421       $cust_contact = new FS::cust_contact { %hash, %link_hash };
422       $error = $cust_contact->insert;
423     }
424     if ( $error ) {
425       $dbh->rollback if $oldAutoCommit;
426       return $error;
427     }
428   }
429
430   if ( $prospectnum ) {
431     my %hash = ( 'contactnum'  => $self->contactnum,
432                  'prospectnum' => $prospectnum,
433                );
434     my $error;
435     if ( my $prospect_contact = qsearchs('prospect_contact', \%hash ) ) {
436       $prospect_contact->$_($link_hash{$_}) for keys %link_hash;
437       $error = $prospect_contact->replace;
438     } else {
439       my $prospect_contact = new FS::prospect_contact { %hash, %link_hash };
440       $error = $prospect_contact->insert;
441     }
442     if ( $error ) {
443       $dbh->rollback if $oldAutoCommit;
444       return $error;
445     }
446   }
447
448   foreach my $pf ( grep { /^phonetypenum(\d+)$/ }
449                         keys %{ $self->hashref } ) {
450     $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
451     my $phonetypenum = $1;
452
453     my %cp = ( 'contactnum'   => $self->contactnum,
454                'phonetypenum' => $phonetypenum,
455              );
456     my $contact_phone = qsearchs('contact_phone', \%cp);
457
458     my $pv = $self->get($pf);
459         $pv =~ s/\s//g;
460
461     #if new value is empty, delete old entry
462     if (!$pv) {
463       if ($contact_phone) {
464         $error = $contact_phone->delete;
465         if ( $error ) {
466           $dbh->rollback if $oldAutoCommit;
467           return $error;
468         }
469       }
470       next;
471     }
472
473     $contact_phone ||= new FS::contact_phone \%cp;
474
475     my %cpd = _parse_phonestring( $pv );
476     $contact_phone->set( $_ => $cpd{$_} ) foreach keys %cpd;
477
478     my $method = $contact_phone->contactphonenum ? 'replace' : 'insert';
479
480     $error = $contact_phone->$method;
481     if ( $error ) {
482       $dbh->rollback if $oldAutoCommit;
483       return $error;
484     }
485   }
486
487   if ( defined($self->hashref->{'emailaddress'}) ) {
488
489     #ineffecient but whatever, how many email addresses can there be?
490
491     foreach my $contact_email ( $self->contact_email ) {
492       my $error = $contact_email->delete;
493       if ( $error ) {
494         $dbh->rollback if $oldAutoCommit;
495         return $error;
496       }
497     }
498
499     foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
500  
501       my $contact_email = new FS::contact_email {
502         'contactnum'   => $self->contactnum,
503         'emailaddress' => $email,
504       };
505       $error = $contact_email->insert;
506       if ( $error ) {
507         $dbh->rollback if $oldAutoCommit;
508         return $error;
509       }
510
511     }
512
513   }
514
515   unless ( $skip_fuzzyfiles ) { #unless ( $import || $skip_fuzzyfiles ) {
516     #warn "  queueing fuzzyfiles update\n"
517     #  if $DEBUG > 1;
518     $error = $self->queue_fuzzyfiles_update;
519     if ( $error ) {
520       $dbh->rollback if $oldAutoCommit;
521       return "updating fuzzy search cache: $error";
522     }
523   }
524
525   if ( $cust_contact and (
526                               (      $cust_contact->selfservice_access eq ''
527                                   && $link_hash{selfservice_access}
528                                   && ! length($self->_password)
529                               )
530                            || $cust_contact->_resend()
531                          )
532     )
533   {
534     my $error = $self->send_reset_email( queue=>1 );
535     if ( $error ) {
536       $dbh->rollback if $oldAutoCommit;
537       return $error;
538     }
539   }
540
541   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
542
543   '';
544
545 }
546
547 =item _parse_phonestring PHONENUMBER_STRING
548
549 Subroutine, takes a string and returns a list (suitable for assigning to a hash)
550 with keys 'countrycode', 'phonenum' and 'extension'
551
552 (Should probably be moved to contact_phone.pm, hence the initial underscore.)
553
554 =cut
555
556 sub _parse_phonestring {
557   my $value = shift;
558
559   my($countrycode, $extension) = ('1', '');
560
561   #countrycode
562   if ( $value =~ s/^\s*\+\s*(\d+)// ) {
563     $countrycode = $1;
564   } else {
565     $value =~ s/^\s*1//;
566   }
567   #extension
568   if ( $value =~ s/\s*(ext|x)\s*(\d+)\s*$//i ) {
569      $extension = $2;
570   }
571
572   ( 'countrycode' => $countrycode,
573     'phonenum'    => $value,
574     'extension'   => $extension,
575   );
576 }
577
578 =item queue_fuzzyfiles_update
579
580 Used by insert & replace to update the fuzzy search cache
581
582 =cut
583
584 use FS::cust_main::Search;
585 sub queue_fuzzyfiles_update {
586   my $self = shift;
587
588   local $SIG{HUP} = 'IGNORE';
589   local $SIG{INT} = 'IGNORE';
590   local $SIG{QUIT} = 'IGNORE';
591   local $SIG{TERM} = 'IGNORE';
592   local $SIG{TSTP} = 'IGNORE';
593   local $SIG{PIPE} = 'IGNORE';
594
595   my $oldAutoCommit = $FS::UID::AutoCommit;
596   local $FS::UID::AutoCommit = 0;
597   my $dbh = dbh;
598
599   foreach my $field ( 'first', 'last' ) {
600     my $queue = new FS::queue { 
601       'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
602     };
603     my @args = "contact.$field", $self->get($field);
604     my $error = $queue->insert( @args );
605     if ( $error ) {
606       $dbh->rollback if $oldAutoCommit;
607       return "queueing job (transaction rolled back): $error";
608     }
609   }
610
611   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
612   '';
613
614 }
615
616 =item check
617
618 Checks all fields to make sure this is a valid contact.  If there is
619 an error, returns the error, otherwise returns false.  Called by the insert
620 and replace methods.
621
622 =cut
623
624 sub check {
625   my $self = shift;
626
627   if ( $self->selfservice_access eq 'R' ) {
628     $self->selfservice_access('Y');
629     $self->_resend('Y');
630   }
631
632   my $error = 
633     $self->ut_numbern('contactnum')
634     || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum')
635     || $self->ut_foreign_keyn('custnum',     'cust_main',     'custnum')
636     || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum')
637     || $self->ut_foreign_keyn('classnum',    'contact_class', 'classnum')
638     || $self->ut_namen('last')
639     || $self->ut_namen('first')
640     || $self->ut_textn('title')
641     || $self->ut_textn('comment')
642     || $self->ut_enum('selfservice_access', [ '', 'Y' ])
643     || $self->ut_textn('_password')
644     || $self->ut_enum('_password_encoding', [ '', 'bcrypt'])
645     || $self->ut_enum('disabled', [ '', 'Y' ])
646   ;
647   return $error if $error;
648
649   return "Prospect and customer!"       if $self->prospectnum && $self->custnum;
650
651   return "One of first name, last name, or title must have a value"
652     if ! grep $self->$_(), qw( first last title);
653
654   $self->SUPER::check;
655 }
656
657 =item line
658
659 Returns a formatted string representing this contact, including name, title and
660 comment.
661
662 =cut
663
664 sub line {
665   my $self = shift;
666   my $data = $self->first. ' '. $self->last;
667   $data .= ', '. $self->title
668     if $self->title;
669   $data .= ' ('. $self->comment. ')'
670     if $self->comment;
671   $data;
672 }
673
674 =item firstlast
675
676 Returns a formatted string representing this contact, with just the name.
677
678 =cut
679
680 sub firstlast {
681   my $self = shift;
682   $self->first . ' ' . $self->last;
683 }
684
685 #=item contact_classname PROSPECT_OBJ | CUST_MAIN_OBJ
686 #
687 #Returns the name of this contact's class for the specified prospect or
688 #customer (see L<FS::prospect_contact>, L<FS::cust_contact> and
689 #L<FS::contact_class>).
690 #
691 #=cut
692 #
693 #sub contact_classname {
694 #  my( $self, $prospect_or_cust ) = @_;
695 #
696 #  my $link = '';
697 #  if ( ref($prospect_or_cust) eq 'FS::prospect_main' ) {
698 #    $link = qsearchs('prospect_contact', {
699 #              'contactnum'  => $self->contactnum,
700 #              'prospectnum' => $prospect_or_cust->prospectnum,
701 #            });
702 #  } elsif ( ref($prospect_or_cust) eq 'FS::cust_main' ) {
703 #    $link = qsearchs('cust_contact', {
704 #              'contactnum'  => $self->contactnum,
705 #              'custnum'     => $prospect_or_cust->custnum,
706 #            });
707 #  } else {
708 #    croak "$prospect_or_cust is not an FS::prospect_main or FS::cust_main object";
709 #  }
710 #
711 #  my $contact_class = $link->contact_class or return '';
712 #  $contact_class->classname;
713 #}
714
715 =item by_selfservice_email EMAILADDRESS
716
717 Alternate search constructor (class method).  Given an email address,
718 returns the contact for that address, or the empty string if no contact
719 has that email address.
720
721 =cut
722
723 sub by_selfservice_email {
724   my($class, $email) = @_;
725
726   my $contact_email = qsearchs({
727     'table'     => 'contact_email',
728     'addl_from' => ' LEFT JOIN contact USING ( contactnum ) ',
729     'hashref'   => { 'emailaddress' => $email, },
730     'extra_sql' => " AND ( disabled IS NULL OR disabled = '' )",
731   }) or return '';
732
733   $contact_email->contact;
734
735 }
736
737 #these three functions are very much false laziness w/FS/FS/Auth/internal.pm
738 # and should maybe be libraried in some way for other password needs
739
740 use Crypt::Eksblowfish::Bcrypt qw( bcrypt_hash en_base64 de_base64);
741
742 sub authenticate_password {
743   my($self, $check_password) = @_;
744
745   if ( $self->_password_encoding eq 'bcrypt' ) {
746
747     my( $cost, $salt, $hash ) = split(',', $self->_password);
748
749     my $check_hash = en_base64( bcrypt_hash( { key_nul => 1,
750                                                cost    => $cost,
751                                                salt    => de_base64($salt),
752                                              },
753                                              $check_password
754                                            )
755                               );
756
757     $hash eq $check_hash;
758
759   } else { 
760
761     return 0 if $self->_password eq '';
762
763     $self->_password eq $check_password;
764
765   }
766
767 }
768
769 sub change_password {
770   my($self, $new_password) = @_;
771
772   $self->change_password_fields( $new_password );
773
774   $self->replace;
775
776 }
777
778 sub change_password_fields {
779   my($self, $new_password) = @_;
780
781   $self->_password_encoding('bcrypt');
782
783   my $cost = 8;
784
785   my $salt = pack( 'C*', map int(rand(256)), 1..16 );
786
787   my $hash = bcrypt_hash( { key_nul => 1,
788                             cost    => $cost,
789                             salt    => $salt,
790                           },
791                           $new_password,
792                         );
793
794   $self->_password(
795     join(',', $cost, en_base64($salt), en_base64($hash) )
796   );
797
798 }
799
800 # end of false laziness w/FS/FS/Auth/internal.pm
801
802
803 #false laziness w/ClientAPI/MyAccount/reset_passwd
804 use Digest::SHA qw(sha512_hex);
805 use FS::Conf;
806 use FS::ClientAPI_SessionCache;
807 sub send_reset_email {
808   my( $self, %opt ) = @_;
809
810   my @contact_email = $self->contact_email or return '';
811
812   my $reset_session = {
813     'contactnum' => $self->contactnum,
814     'svcnum'     => $opt{'svcnum'},
815   };
816
817   my $timeout = '24 hours'; #?
818
819   my $reset_session_id;
820   do {
821     $reset_session_id = sha512_hex(time(). {}. rand(). $$)
822   } until ( ! defined $self->myaccount_cache->get("reset_passwd_$reset_session_id") );
823     #just in case
824
825   $self->myaccount_cache->set( "reset_passwd_$reset_session_id", $reset_session, $timeout );
826
827   #email it
828
829   my $conf = new FS::Conf;
830
831   my $cust_main = '';
832   my @cust_contact = grep $_->selfservice_access, $self->cust_contact;
833   $cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1;
834
835   my $agentnum = $cust_main ? $cust_main->agentnum : '';
836   my $msgnum = $conf->config('selfservice-password_reset_msgnum', $agentnum);
837   #die "selfservice-password_reset_msgnum unset" unless $msgnum;
838   return { 'error' => "selfservice-password_reset_msgnum unset" } unless $msgnum;
839   my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
840   my %msg_template = (
841     'to'            => join(',', map $_->emailaddress, @contact_email ),
842     'cust_main'     => $cust_main,
843     'object'        => $self,
844     'substitutions' => { 'session_id' => $reset_session_id }
845   );
846
847   if ( $opt{'queue'} ) { #or should queueing just be the default?
848
849     my $queue = new FS::queue {
850       'job'     => 'FS::Misc::process_send_email',
851       'custnum' => $cust_main ? $cust_main->custnum : '',
852     };
853     $queue->insert( $msg_template->prepare( %msg_template ) );
854
855   } else {
856
857     $msg_template->send( %msg_template );
858
859   }
860
861 }
862
863 use vars qw( $myaccount_cache );
864 sub myaccount_cache {
865   #my $class = shift;
866   $myaccount_cache ||= new FS::ClientAPI_SessionCache( {
867                          'namespace' => 'FS::ClientAPI::MyAccount',
868                        } );
869 }
870
871 =item cgi_contact_fields
872
873 Returns a list reference containing the set of contact fields used in the web
874 interface for one-line editing (i.e. excluding contactnum, prospectnum, custnum
875 and locationnum, as well as password fields, but including fields for
876 contact_email and contact_phone records.)
877
878 =cut
879
880 sub cgi_contact_fields {
881   #my $class = shift;
882
883   my @contact_fields = qw(
884     classnum first last title comment emailaddress selfservice_access
885   );
886
887   push @contact_fields, 'phonetypenum'. $_->phonetypenum
888     foreach qsearch({table=>'phone_type', order_by=>'weight'});
889
890   \@contact_fields;
891
892 }
893
894 use FS::upgrade_journal;
895 sub _upgrade_data { #class method
896   my ($class, %opts) = @_;
897
898   unless ( FS::upgrade_journal->is_done('contact__DUPEMAIL') ) {
899
900     foreach my $contact (qsearch('contact', {})) {
901       my $error = $contact->replace;
902       die $error if $error;
903     }
904
905     FS::upgrade_journal->set_done('contact__DUPEMAIL');
906   }
907
908 }
909
910 =back
911
912 =head1 BUGS
913
914 =head1 SEE ALSO
915
916 L<FS::Record>, schema.html from the base documentation.
917
918 =cut
919
920 1;
921