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