1 // Copyright 2009 The Archiveopteryx Developers <info@aox.org>
2 
3 #include "users.h"
4 
5 #include "utf.h"
6 #include "user.h"
7 #include "query.h"
8 #include "address.h"
9 #include "mailbox.h"
10 #include "integerset.h"
11 #include "transaction.h"
12 #include "helperrowcreator.h"
13 
14 #include <stdio.h>
15 #include <stdlib.h>
16 
17 
18 static AoxFactory<ListUsers>
19 f2( "list", "users", "Display existing users.",
20     "    Synopsis: aox list users [pattern]\n\n"
21     "    Displays a list of users matching the specified shell\n"
22     "    glob pattern. Without a pattern, all users are listed.\n\n"
23     "    ls is an acceptable abbreviation for list.\n\n"
24     "    Examples:\n\n"
25     "      aox list users\n"
26     "      aox ls users ab?cd*\n" );
27 
28 
29 /*! \class ListUsers users.h
30     This class handles the "aox list users" command.
31 */
32 
ListUsers(EStringList * args)33 ListUsers::ListUsers( EStringList * args )
34     : AoxCommand( args ), q( 0 )
35 {
36 }
37 
38 
execute()39 void ListUsers::execute()
40 {
41     if ( !q ) {
42         Utf8Codec c;
43         UString pattern = c.toUnicode( next() );
44         end();
45 
46         if ( !c.valid() )
47             error( "Argument encoding: " + c.error() );
48 
49         database();
50         EString s( "select login, (localpart||'@'||domain)::text as address "
51                   "from users u join aliases al on (u.alias=al.id) "
52                   "join addresses a on (al.address=a.id)" );
53         if ( !pattern.isEmpty() )
54             s.append( " where login like $1" );
55         q = new Query( s, this );
56         if ( !pattern.isEmpty() )
57             q->bind( 1, sqlPattern( pattern ) );
58         q->execute();
59     }
60 
61     while ( q->hasResults() ) {
62         Row * r = q->nextRow();
63         printf( "%-16s %s\n",
64                 r->getUString( "login" ).utf8().cstr(),
65                 r->getEString( "address" ).cstr() );
66     }
67 
68     if ( !q->done() )
69         return;
70 
71     finish();
72 }
73 
74 
75 
76 class CreateUserData
77     : public Garbage
78 {
79 public:
CreateUserData()80     CreateUserData()
81         : user( 0 ), query( 0 )
82     {}
83 
84     User * user;
85     Query * query;
86 };
87 
88 
89 static AoxFactory<CreateUser>
90 f3( "create", "user", "Create a new user.",
91     "    Synopsis:\n"
92     "      aox add user <username> <password> <email-address>\n"
93     "      aox add user -p <username> <email-address>\n\n"
94     "    Creates a new Archiveopteryx user with the given username,\n"
95     "    password, and email address.\n\n"
96     "    The -p flag causes the password to be read interactively, and\n"
97     "    not from the command line.\n\n"
98     "    Examples:\n\n"
99     "      aox add user nirmala secret nirmala@example.org\n" );
100 
101 
102 /*! \class CreateUser users.h
103     This class handles the "aox add user" command.
104 */
105 
CreateUser(EStringList * args)106 CreateUser::CreateUser( EStringList * args )
107     : AoxCommand( args ), d( new CreateUserData )
108 {
109 }
110 
111 
execute()112 void CreateUser::execute()
113 {
114     if ( !d->user ) {
115         parseOptions();
116         Utf8Codec c;
117         UString login = c.toUnicode( next() );
118 
119         UString passwd;
120         if ( opt( 'p' ) == 0 )
121             passwd = c.toUnicode( next() );
122         else
123             passwd = c.toUnicode( readNewPassword() );
124 
125         EString address = next();
126         end();
127 
128         if ( !c.valid() )
129             error( "Argument encoding: " + c.error() );
130         if ( login.isEmpty() || passwd.isEmpty() || address.isEmpty() )
131             error( "Username, password, and address must be non-empty." );
132         if ( !validUsername( login ) )
133             error( "Invalid username: " + login.utf8() );
134 
135         AddressParser p( address );
136         p.assertSingleAddress();
137         if ( !p.error().isEmpty() )
138             error( "Invalid address: " + p.error() );
139 
140         Address * a = p.addresses()->first();
141         if ( Configuration::toggle( Configuration::UseSubaddressing ) ) {
142             EString lp = a->localpart().utf8();
143             if ( Configuration::present( Configuration::AddressSeparator ) ) {
144                 Configuration::Text t = Configuration::AddressSeparator;
145                 if ( lp.contains(Configuration::text( t ) ) ) {
146                     error( "Localpart cannot contain subaddress separator" );
147                 }
148             }
149             else if ( lp.contains( "-" ) ) {
150                 error( "Localpart cannot contain subaddress separator '-'" );
151             }
152             else if ( lp.contains( "+" ) ) {
153                 error( "Localpart cannot contain subaddress separator '+'" );
154             }
155         }
156 
157         database( true );
158         Mailbox::setup( this );
159 
160         d->user = new User;
161         d->user->setLogin( login );
162         d->user->setSecret( passwd );
163         d->user->setAddress( a );
164         d->user->refresh( this );
165     }
166 
167     if ( !choresDone() )
168         return;
169 
170     if ( !d->query ) {
171         if ( d->user->state() == User::Unverified )
172             return;
173 
174         if ( d->user->state() != User::Nonexistent )
175             error( "User " + d->user->login().utf8() + " already exists." );
176 
177         d->query = d->user->create( this );
178         d->user->execute();
179     }
180 
181     if ( !d->query->done() )
182         return;
183 
184     if ( d->query->failed() )
185         error( "Couldn't create user: " + d->query->error() );
186 
187     finish();
188 }
189 
190 
191 
192 class DeleteUserData
193     : public Garbage
194 {
195 public:
DeleteUserData()196     DeleteUserData()
197         : user( 0 ), t( 0 ), query( 0 ), processed( false )
198     {}
199 
200     User * user;
201     Transaction * t;
202     Query * query;
203     bool processed;
204 };
205 
206 
207 static AoxFactory<DeleteUser>
208 f4( "delete", "user", "Delete a user.",
209     "    Synopsis: aox delete user [-f] <username>\n\n"
210     "    Deletes the Archiveopteryx user with the specified name.\n\n"
211     "    The -f flag causes any mailboxes owned by the user to be deleted\n"
212     "    (even if they aren't empty).\n" );
213 
214 
215 /*! \class DeleteUser users.h
216     This class handles the "aox delete user" command.
217 */
218 
DeleteUser(EStringList * args)219 DeleteUser::DeleteUser( EStringList * args )
220     : AoxCommand( args ), d( new DeleteUserData )
221 {
222 }
223 
224 
execute()225 void DeleteUser::execute()
226 {
227     if ( !d->user ) {
228         parseOptions();
229         Utf8Codec c;
230         UString login = c.toUnicode( next() );
231         end();
232 
233         if ( !c.valid() )
234             error( "Argument encoding: " + c.error() );
235         if ( login.isEmpty() )
236             error( "No username supplied." );
237         if ( !validUsername( login ) )
238             error( "Invalid username: " + login.utf8() );
239 
240         database( true );
241         Mailbox::setup( this );
242 
243         d->user = new User;
244         d->user->setLogin( login );
245         d->user->refresh( this );
246 
247         d->t = new Transaction( this );
248 
249         d->query =
250             new Query(
251                 "select m.id, "
252                 "exists(select message from mailbox_messages where mailbox=m.id)"
253                 " as nonempty "
254                 "from mailboxes m join users u on (m.owner=u.id) where u.login=$1 "
255                 "for update",
256                 this );
257         d->query->bind( 1, login );
258         d->t->enqueue( d->query );
259         d->t->execute();
260     }
261 
262     if ( !choresDone() )
263         return;
264 
265     if ( d->user->state() == User::Unverified )
266         return;
267 
268     if ( d->user->state() == User::Nonexistent )
269         error( "No user named " + d->user->login().utf8() );
270 
271     if ( !d->query->done() )
272         return;
273 
274     if ( !d->processed ) {
275 
276         d->processed = true;
277 
278         IntegerSet all;
279         IntegerSet nonempty;
280         while ( d->query->hasResults() ) {
281             Row * r = d->query->nextRow();
282             if ( r->getBoolean( "nonempty" ) )
283                 nonempty.add( r->getInt( "id" ) );
284             all.add( r->getInt( "id" ) );
285         }
286 
287         if ( nonempty.isEmpty() ) {
288             // we silently delete empty mailboxes, only actual mail matters to us
289         }
290         else if ( opt( 'f' ) ) {
291             Query * q = new Query( "insert into deleted_messages "
292                                    "(mailbox, uid, message, modseq,"
293                                    " deleted_by, reason) "
294                                    "select mm.mailbox, mm.uid, mm.message,"
295                                    " mb.nextmodseq, null,"
296                                    " 'aox delete user -f' "
297                                    "from mailbox_messages mm "
298                                    "join mailboxes mb on (mm.mailbox=mb.id) "
299                                    "where mb.id=any($1)", 0 );
300             q->bind( 1, nonempty );
301             d->t->enqueue( q );
302         }
303         else {
304             fprintf( stderr, "User %s still owns the following nonempty mailboxes:\n",
305                      d->user->login().utf8().cstr() );
306             uint n = 1;
307             while ( n <= nonempty.count() ) {
308                 Mailbox * m = Mailbox::find( nonempty.value( n ) );
309                 ++n;
310                 if ( m )
311                     fprintf( stderr, "    %s\n", m->name().utf8().cstr() );
312             }
313             fprintf( stderr, "(Use 'aox delete user -f %s' to delete these "
314                      "mailboxes too.)\n", d->user->login().utf8().cstr() );
315             exit( -1 );
316         }
317 
318         if ( !all.isEmpty() ) {
319             Query * q;
320 
321             q = new Query( "update users set alias=null where id=$1", 0 );
322             q->bind( 1, d->user->id() );
323             d->t->enqueue( q );
324 
325             q = new Query( "delete from aliases where mailbox=any($1)", 0 );
326             q->bind( 1, all );
327             d->t->enqueue( q );
328 
329             q = new Query( "update mailboxes set deleted='t',owner=null "
330                            "where owner=$1 and id=any($2)",
331                            0 );
332             q->bind( 1, d->user->id() );
333             q->bind( 2, all );
334             d->t->enqueue( q );
335 
336             q = new Query( "update deleted_messages set deleted_by=null "
337                            "where deleted_by=$1",
338                            0 );
339             q->bind( 1, d->user->id() );
340             d->t->enqueue( q );
341 
342         }
343 
344         d->user->remove( d->t );
345 
346         d->t->commit();
347     }
348 
349     if ( !d->t->done() )
350         return;
351 
352     if ( d->t->failed() )
353         error( "Couldn't delete user" );
354 
355     finish();
356 }
357 
358 
359 
360 static AoxFactory<ChangePassword>
361 f( "change", "password", "Change a user's password.",
362    "    Synopsis:\n"
363    "      aox change password <username> <new-password>\n"
364    "      aox change password -p <username>\n\n"
365    "    Changes the specified user's password.\n\n"
366    "    The -p flag causes the password to be read interactively, and\n"
367    "    not from the command line.\n\n" );
368 
369 
370 /*! \class ChangePassword users.h
371     This class handles the "aox change password" command.
372 */
373 
ChangePassword(EStringList * args)374 ChangePassword::ChangePassword( EStringList * args )
375     : AoxCommand( args ), q( 0 )
376 {
377 }
378 
379 
execute()380 void ChangePassword::execute()
381 {
382     if ( !q ) {
383         parseOptions();
384         Utf8Codec c;
385         UString login = c.toUnicode( next() );
386 
387         UString passwd;
388         if ( opt( 'p' ) == 0 )
389             passwd = c.toUnicode( next() );
390         else
391             passwd = c.toUnicode( readNewPassword() );
392         end();
393 
394         if ( !c.valid() )
395             error( "Argument encoding: " + c.error() );
396         if ( login.isEmpty() || passwd.isEmpty() )
397             error( "No username and password supplied." );
398         if ( !validUsername( login ) )
399             error( "Invalid username: " + login.utf8() );
400 
401         database( true );
402 
403         User * u = new User;
404         u->setLogin( login );
405         u->setSecret( passwd );
406         q = u->changeSecret( this );
407         if ( !q->failed() )
408             u->execute();
409     }
410 
411     if ( !q->done() )
412         return;
413 
414     if ( q->failed() )
415         error( "Couldn't change password" );
416 
417     finish();
418 }
419 
420 
421 
422 class ChangeUsernameData
423     : public Garbage
424 {
425 public:
ChangeUsernameData()426     ChangeUsernameData()
427         : user( 0 ), t( 0 ), query( 0 )
428     {}
429 
430     User * user;
431     UString newname;
432     Transaction * t;
433     Query * query;
434 };
435 
436 
437 static AoxFactory<ChangeUsername>
438 f5( "change", "username", "Change a user's name.",
439     "    Synopsis: aox change username <username> <new-username>\n\n"
440     "    Changes the specified user's username.\n" );
441 
442 
443 /*! \class ChangeUsername users.h
444     This class handles the "aox change username" command.
445 */
446 
ChangeUsername(EStringList * args)447 ChangeUsername::ChangeUsername( EStringList * args )
448     : AoxCommand( args ), d( new ChangeUsernameData )
449 {
450 }
451 
452 
execute()453 void ChangeUsername::execute()
454 {
455     if ( !d->user ) {
456         parseOptions();
457         Utf8Codec c;
458         UString name = c.toUnicode( next() );
459         d->newname = c.toUnicode( next() );
460         end();
461 
462         if ( !c.valid() )
463             error( "Argument encoding: " + c.error() );
464         if ( name.isEmpty() || d->newname.isEmpty() )
465             error( "Old and new usernames not supplied." );
466         if ( !validUsername( name ) )
467             error( "Invalid username: " + name.utf8() );
468         if ( !validUsername( d->newname ) )
469             error( "Invalid username: " + d->newname.utf8() );
470 
471         database( true );
472         Mailbox::setup( this );
473 
474         d->user = new User;
475         d->user->setLogin( name );
476         d->user->refresh( this );
477     }
478 
479     if ( !choresDone() )
480         return;
481 
482     if ( !d->t ) {
483         if ( d->user->state() == User::Unverified )
484             return;
485 
486         if ( d->user->state() == User::Nonexistent )
487             error( "No user named " + d->user->login().utf8() );
488 
489         d->t = new Transaction( this );
490 
491         Query * q =
492             new Query( "update users set login=$2 where id=$1", this );
493         q->bind( 1, d->user->id() );
494         q->bind( 2, d->newname );
495         d->t->enqueue( q );
496 
497         d->query =
498             new Query( "select name from mailboxes where deleted='f' and "
499                        "(name ilike '/users/'||$1||'/%' or"
500                        " name ilike '/users/'||$1)", this );
501         d->query->bind( 1, d->user->login() );
502         d->t->enqueue( d->query );
503 
504         d->t->execute();
505     }
506 
507     if ( d->query && d->query->done() ) {
508         while ( d->query->hasResults() ) {
509             Row * r = d->query->nextRow();
510 
511             UString name = r->getUString( "name" );
512             UString newname = name;
513             int i = name.find( '/', 1 );
514             newname.truncate( i+1 );
515             newname.append( d->newname );
516             i = name.find( '/', i+1 );
517             if ( i >= 0 )
518                 newname.append( name.mid( i ) );
519 
520             Query * q;
521 
522             Mailbox * from = Mailbox::obtain( name );
523             uint uidvalidity = from->uidvalidity();
524 
525             Mailbox * to = Mailbox::obtain( newname );
526             if ( to->deleted() ) {
527                 if ( to->uidvalidity() > uidvalidity ||
528                      to->uidnext() > 1 )
529                     uidvalidity = to->uidvalidity() + 1;
530                 q = new Query( "delete from mailboxes where id=$1", this );
531                 q->bind( 1, to->id() );
532                 d->t->enqueue( q );
533             }
534 
535             q = new Query( "update mailboxes set name=$2,uidvalidity=$3 "
536                            "where id=$1", this );
537             q->bind( 1, from->id() );
538             q->bind( 2, newname );
539             q->bind( 3, uidvalidity );
540 
541             d->t->enqueue( q );
542         }
543 
544         d->t->commit();
545         d->query = 0;
546     }
547 
548     if ( !d->t->done() )
549         return;
550 
551     if ( d->t->failed() )
552         error( "Couldn't change username" );
553 
554     finish();
555 }
556 
557 
558 
559 class ChangeAddressData
560     : public Garbage
561 {
562 public:
ChangeAddressData()563     ChangeAddressData()
564         : user( 0 ), address( 0 ), t( 0 ), query( 0 )
565     {}
566 
567     User * user;
568     Address * address;
569     Transaction * t;
570     Query * query;
571 };
572 
573 
574 static AoxFactory<ChangeAddress>
575 f6( "change", "address", "Change a user's email address.",
576     "    Synopsis: aox change address <username> <new-address>\n\n"
577     "    Changes the specified user's email address.\n" );
578 
579 
580 /*! \class ChangeAddress users.h
581     This class handles the "aox change address" command.
582 */
583 
ChangeAddress(EStringList * args)584 ChangeAddress::ChangeAddress( EStringList * args )
585     : AoxCommand( args ), d( new ChangeAddressData )
586 {
587 }
588 
589 
execute()590 void ChangeAddress::execute()
591 {
592     if ( !d->user ) {
593         parseOptions();
594         Utf8Codec c;
595         UString name = c.toUnicode( next() );
596         EString address = next();
597         end();
598 
599         if ( !c.valid() )
600             error( "Argument encoding: " + c.error() );
601         if ( name.isEmpty() || address.isEmpty() )
602             error( "Username and address must be non-empty." );
603         if ( !validUsername( name ) )
604             error( "Invalid username: " + name.utf8() );
605 
606         AddressParser p( address );
607         p.assertSingleAddress();
608         if ( !p.error().isEmpty() )
609             error( "Invalid address: " + p.error() );
610 
611         database( true );
612         Mailbox::setup( this );
613 
614         d->address = p.addresses()->first();
615         d->user = new User;
616         d->user->setLogin( name );
617         d->user->refresh( this );
618     }
619 
620     if ( !choresDone() )
621         return;
622 
623     if ( !d->t ) {
624         if ( d->user->state() == User::Unverified )
625             return;
626 
627         if ( d->user->state() == User::Nonexistent )
628             error( "No user named " + d->user->login().utf8() );
629 
630         d->t = new Transaction( this );
631         AddressCreator * ac = new AddressCreator( d->address, d->t );
632         ac->execute();
633     }
634 
635     if ( d->address->id() == 0 )
636         return;
637 
638     if ( !d->query ) {
639         d->query =
640             new Query( "update aliases set address=$2 where id="
641                        "(select alias from users where id=$1)", this );
642         d->query->bind( 1, d->user->id() );
643         d->query->bind( 2, d->address->id() );
644         d->t->enqueue( d->query );
645         d->t->commit();
646     }
647 
648     if ( !d->t->done() )
649         return;
650 
651     if ( d->t->failed() )
652         error( "Couldn't change address" );
653 
654     finish();
655 }
656