2018-01-22 13:01:38 -08:00
'use strict' ;
2020-06-03 23:17:06 +02:00
/* global angular */
/* global $ */
/* global async */
2019-01-22 10:54:03 +01:00
2021-11-19 15:45:16 +01:00
angular . module ( 'Application' ) . controller ( 'EmailController' , [ '$scope' , '$location' , '$translate' , '$timeout' , '$route' , '$routeParams' , 'Client' , function ( $scope , $location , $translate , $timeout , $route , $routeParams , Client ) {
2021-12-02 09:29:33 -08:00
Client . onReady ( function ( ) { if ( ! Client . getUserInfo ( ) . isAtLeastMailManager ) $location . path ( '/' ) ; } ) ;
$scope . user = Client . getUserInfo ( ) ;
2018-01-22 13:01:38 -08:00
2021-11-19 15:45:16 +01:00
// Avoid full reload on path change
// https://stackoverflow.com/a/22614334
// reloadOnUrl: false in $routeProvider did not work!
var lastRoute = $route . current ;
$scope . $on ( '$locationChangeSuccess' , function ( /* event */ ) {
if ( lastRoute . $$route . originalPath === $route . current . $$route . originalPath ) {
$route . current = lastRoute ;
}
} ) ;
2020-02-11 21:06:34 +01:00
var domainName = $routeParams . domain ;
if ( ! domainName ) return $location . path ( '/email' ) ;
2021-11-19 15:45:16 +01:00
$scope . setView = function ( view , setAlways ) {
if ( ! setAlways && ! $scope . ready ) return ;
if ( $scope . view === view ) return ;
$route . updateParams ( { view : view } ) ;
$scope . view = view ;
$scope . activeTab = view ;
} ;
2018-01-23 12:30:35 +01:00
$scope . ready = false ;
$scope . refreshBusy = true ;
2018-01-22 13:01:38 -08:00
$scope . client = Client ;
$scope . user = Client . getUserInfo ( ) ;
$scope . config = Client . getConfig ( ) ;
2018-12-06 10:28:42 -08:00
$scope . apps = Client . getInstalledApps ( ) ;
2020-11-12 23:53:02 -08:00
$scope . owners = [ ] ; // users + groups
2020-04-19 19:44:45 -07:00
$scope . incomingDomains = [ ] ;
2020-02-11 21:06:34 +01:00
$scope . domain = null ;
2019-12-31 10:33:09 -08:00
$scope . adminDomain = null ;
2023-01-24 18:46:42 +01:00
$scope . mailUsage = null ;
2024-03-12 15:43:08 +01:00
$scope . storageQuotaTicks = [ 500 * 1000 * 1000 , 5 * 1000 * 1000 * 1000 , 15 * 1000 * 1000 * 1000 , 50 * 1000 * 1000 * 1000 , 100 * 1000 * 1000 * 1000 ] ;
2018-01-23 12:30:35 +01:00
2018-01-22 13:01:38 -08:00
$scope . expectedDnsRecords = {
mx : { } ,
dkim : { } ,
spf : { } ,
dmarc : { } ,
ptr : { }
} ;
2020-07-16 18:51:29 +02:00
2018-01-22 13:01:38 -08:00
$scope . expectedDnsRecordsTypes = [
{ name : 'MX' , value : 'mx' } ,
{ name : 'DKIM' , value : 'dkim' } ,
{ name : 'SPF' , value : 'spf' } ,
{ name : 'DMARC' , value : 'dmarc' } ,
{ name : 'PTR' , value : 'ptr' }
] ;
2020-07-16 18:51:29 +02:00
$scope . openSubscriptionSetup = function ( ) {
Client . openSubscriptionSetup ( $scope . $parent . subscription ) ;
} ;
2023-06-15 20:15:33 +05:30
function updateMailUsage ( mailboxName , quotaLimit ) {
if ( ! $scope . mailUsage ) $scope . mailUsage = { } ;
if ( ! $scope . mailUsage [ mailboxName ] ) $scope . mailUsage [ mailboxName ] = { } ;
$scope . mailUsage [ mailboxName ] . quotaLimit = quotaLimit ;
}
2023-01-24 18:46:42 +01:00
function refreshMailUsage ( ) {
Client . getMailUsage ( $scope . domain . domain , function ( error , usage ) {
if ( error ) console . error ( error ) ;
$scope . mailUsage = usage || null ; // if mail server is down, don't stop the listing
} ) ;
}
2018-01-22 13:01:38 -08:00
$scope . catchall = {
2018-04-12 13:02:32 +02:00
mailboxes : [ ] ,
2020-12-18 16:56:09 +01:00
availableMailboxes : [ ] ,
2018-01-22 13:01:38 -08:00
busy : false ,
submit : function ( ) {
$scope . catchall . busy = true ;
2022-09-11 13:48:53 +02:00
var addresses = $scope . catchall . mailboxes . map ( function ( m ) { return m . name + '@' + m . domain ; } ) ;
2018-04-12 13:02:32 +02:00
2020-02-11 21:06:34 +01:00
Client . setCatchallAddresses ( $scope . domain . domain , addresses , function ( error ) {
2018-01-22 13:01:38 -08:00
if ( error ) console . error ( 'Unable to add catchall address.' , error ) ;
2019-01-31 12:24:27 -08:00
$timeout ( function ( ) { $scope . catchall . busy = false ; } , 2000 ) ; // otherwise, it's too fast
2018-01-22 13:01:38 -08:00
} ) ;
2018-04-12 13:02:32 +02:00
} ,
refresh : function ( ) {
2022-09-11 13:48:53 +02:00
var allMailboxes = [ ] ;
async . eachSeries ( $scope . incomingDomains , function ( domain , iteratorDone ) {
2022-09-28 22:10:43 +02:00
2022-09-11 13:48:53 +02:00
Client . listMailboxes ( domain . domain , '' , 1 , 1000 , function ( error , result ) {
if ( error ) return console . error ( error ) ;
2020-07-05 12:18:34 -07:00
2022-09-11 13:48:53 +02:00
result . forEach ( function ( r ) { r . display = r . name + '@' + r . domain ; } ) ;
allMailboxes = allMailboxes . concat ( result ) ;
iteratorDone ( ) ;
} ) ;
} , function ( ) {
$scope . catchall . availableMailboxes = allMailboxes ;
2020-12-18 16:56:09 +01:00
2022-09-11 13:48:53 +02:00
$scope . catchall . mailboxes = $scope . domain . mailConfig . catchAll . map ( function ( address ) {
2022-09-28 22:10:43 +02:00
return allMailboxes . find ( function ( m ) { return m . display === address ; } ) ;
2022-09-11 13:48:53 +02:00
} ) . filter ( function ( m ) { return ! ! m ; } ) ; // remove not found addresses
2020-07-05 12:18:34 -07:00
} ) ;
2018-01-22 13:01:38 -08:00
}
} ;
2018-04-01 21:58:12 +02:00
$scope . mailinglists = {
busy : false ,
2018-04-06 16:43:43 +02:00
mailinglists : [ ] ,
2019-10-22 12:47:32 +02:00
search : '' ,
2020-07-05 11:55:17 -07:00
currentPage : 1 ,
perPage : 10 ,
2018-04-01 21:58:12 +02:00
2018-04-06 16:43:43 +02:00
add : {
busy : false ,
2019-09-11 14:09:53 -07:00
error : { } ,
2018-04-06 16:43:43 +02:00
name : '' ,
2019-09-11 14:09:53 -07:00
membersTxt : '' ,
2020-04-17 17:37:19 -07:00
membersOnly : false ,
2018-04-06 16:43:43 +02:00
2018-04-09 16:12:18 +02:00
reset : function ( ) {
$scope . mailinglists . add . busy = false ;
2019-09-11 14:09:53 -07:00
$scope . mailinglists . add . error = { } ;
2018-04-09 16:12:18 +02:00
$scope . mailinglists . add . name = '' ;
2019-09-11 14:09:53 -07:00
$scope . mailinglists . add . membersTxt = '' ;
2018-04-09 16:12:18 +02:00
} ,
show : function ( ) {
$scope . mailinglists . add . reset ( ) ;
$ ( '#mailinglistAddModal' ) . modal ( 'show' ) ;
} ,
2018-04-06 16:43:43 +02:00
submit : function ( ) {
$scope . mailinglists . add . busy = true ;
2018-04-01 21:58:12 +02:00
2019-09-11 14:09:53 -07:00
var members = $scope . mailinglists . add . membersTxt
. split ( /[\n,]/ )
. map ( function ( m ) { return m . trim ( ) ; } )
. filter ( function ( m ) { return m . length !== 0 ; } ) ;
2018-04-12 11:57:15 +02:00
2020-04-17 17:37:19 -07:00
Client . addMailingList ( $scope . domain . domain , $scope . mailinglists . add . name , members , $scope . mailinglists . add . membersOnly , function ( error ) {
2019-09-11 14:09:53 -07:00
$scope . mailinglists . add . busy = false ;
$scope . mailinglists . add . error = { } ;
2018-04-06 16:43:43 +02:00
if ( error ) {
2019-09-11 14:09:53 -07:00
if ( error . statusCode === 400 && error . message . indexOf ( 'member' ) !== - 1 ) {
$scope . mailinglists . add . error . members = error . message ;
} else {
$scope . mailinglists . add . error . name = error . message ;
}
2018-04-06 16:43:43 +02:00
return ;
}
2018-04-09 16:12:18 +02:00
$scope . mailinglists . add . reset ( ) ;
2018-04-06 16:43:43 +02:00
$scope . mailinglists . refresh ( ) ;
2018-04-09 16:12:18 +02:00
$ ( '#mailinglistAddModal' ) . modal ( 'hide' ) ;
2018-04-01 21:58:12 +02:00
} ) ;
2018-04-06 16:43:43 +02:00
}
} ,
2018-04-09 15:01:12 +02:00
edit : {
busy : false ,
2019-09-11 14:09:53 -07:00
error : { } ,
2018-04-12 12:02:20 +02:00
name : '' ,
2019-09-11 14:09:53 -07:00
membersTxt : '' ,
2020-04-17 17:37:19 -07:00
membersOnly : false ,
2021-04-14 22:37:59 -07:00
active : true ,
2018-04-09 15:01:12 +02:00
show : function ( list ) {
2018-04-12 12:02:20 +02:00
$scope . mailinglists . edit . name = list . name ;
2019-09-11 14:09:53 -07:00
$scope . mailinglists . edit . membersTxt = list . members . sort ( ) . join ( '\n' ) ;
2020-04-17 17:37:19 -07:00
$scope . mailinglists . edit . membersOnly = list . membersOnly ;
2021-04-14 22:37:59 -07:00
$scope . mailinglists . edit . active = list . active ;
2018-04-09 15:01:12 +02:00
$ ( '#mailinglistEditModal' ) . modal ( 'show' ) ;
} ,
submit : function ( ) {
$scope . mailinglists . edit . busy = true ;
2019-09-11 14:09:53 -07:00
var members = $scope . mailinglists . edit . membersTxt . split ( /[\n,]/ )
. map ( function ( m ) { return m . trim ( ) ; } )
. filter ( function ( m ) { return m . length !== 0 ; } ) ;
2018-04-12 12:02:20 +02:00
2021-04-14 22:37:59 -07:00
Client . updateMailingList ( $scope . domain . domain , $scope . mailinglists . edit . name , members , $scope . mailinglists . edit . membersOnly , $scope . mailinglists . edit . active , function ( error ) {
2018-04-09 15:01:12 +02:00
$scope . mailinglists . edit . busy = false ;
2019-09-11 14:09:53 -07:00
$scope . mailinglists . edit . error = { } ;
2018-04-09 15:01:12 +02:00
2019-09-11 14:09:53 -07:00
if ( error ) {
$scope . mailinglists . edit . error . members = error . message ;
return ;
}
2018-04-09 15:01:12 +02:00
2018-04-12 12:12:22 +02:00
$scope . mailinglists . refresh ( ) ;
2018-04-09 15:01:12 +02:00
$ ( '#mailinglistEditModal' ) . modal ( 'hide' ) ;
} ) ;
}
} ,
2018-04-06 16:43:43 +02:00
remove : {
busy : false ,
list : null ,
show : function ( list ) {
$scope . mailinglists . remove . list = list ;
$ ( '#mailinglistRemoveModal' ) . modal ( 'show' ) ;
} ,
submit : function ( ) {
$scope . mailinglists . remove . busy = true ;
2020-02-11 21:06:34 +01:00
Client . removeMailingList ( $scope . domain . domain , $scope . mailinglists . remove . list . name , function ( error ) {
2018-04-06 16:43:43 +02:00
$scope . mailinglists . remove . busy = false ;
if ( error ) return console . error ( error ) ;
$scope . mailinglists . remove . list = null ;
$scope . mailinglists . refresh ( ) ;
$ ( '#mailinglistRemoveModal' ) . modal ( 'hide' ) ;
2018-04-01 21:58:12 +02:00
} ) ;
2018-04-06 16:43:43 +02:00
}
} ,
2018-04-12 13:02:32 +02:00
refresh : function ( callback ) {
callback = typeof callback === 'function' ? callback : function ( error ) { if ( error ) return console . error ( error ) ; } ;
2020-07-05 11:55:17 -07:00
Client . listMailingLists ( $scope . domain . domain , $scope . mailinglists . search , $scope . mailinglists . currentPage , $scope . mailinglists . perPage , function ( error , result ) {
2018-04-12 13:02:32 +02:00
if ( error ) return callback ( error ) ;
2018-04-06 16:43:43 +02:00
2018-04-12 12:15:23 +02:00
$scope . mailinglists . mailinglists = result ;
2018-04-12 13:02:32 +02:00
callback ( ) ;
2018-04-01 21:58:12 +02:00
} ) ;
2020-07-05 11:55:17 -07:00
} ,
showNextPage : function ( ) {
$scope . mailinglists . currentPage ++ ;
$scope . mailinglists . refresh ( ) ;
} ,
showPrevPage : function ( ) {
if ( $scope . mailinglists . currentPage > 1 ) $scope . mailinglists . currentPage -- ;
else $scope . mailinglists . currentPage = 1 ;
$scope . mailinglists . refresh ( ) ;
} ,
updateFilter : function ( fresh ) {
if ( fresh ) $scope . mailinglists . currentPage = 1 ;
$scope . mailinglists . refresh ( ) ;
2018-04-01 21:58:12 +02:00
}
} ;
2020-02-27 10:47:14 -08:00
$scope . mailFromValidation = {
busy : false ,
submit : function ( ) {
$scope . mailFromValidation . busy = true ;
Client . setMailFromValidation ( $scope . domain . domain , ! $scope . domain . mailConfig . mailFromValidation , function ( error ) {
if ( error ) {
$scope . mailFromValidation . busy = false ;
return console . error ( error ) ;
}
// give sometime for the mail container to restart
$timeout ( function ( ) {
$scope . mailFromValidation . busy = false ;
$scope . refreshDomain ( ) ;
} , 5000 ) ;
} ) ;
}
2018-05-23 23:34:09 -07:00
} ;
2020-08-24 10:16:11 -07:00
$scope . banner = {
busy : false ,
text : '' ,
html : '' ,
submit : function ( ) {
$scope . banner . busy = true ;
Client . setMailBanner ( $scope . domain . domain , { text : $scope . banner . text , html : $scope . banner . html } , function ( error ) {
if ( error ) {
$scope . banner . busy = false ;
return console . error ( error ) ;
}
// give sometime for the mail container to restart
$timeout ( function ( ) {
$scope . banner . busy = false ;
$scope . refreshDomain ( ) ;
} , 5000 ) ;
} ) ;
}
} ;
2018-07-30 11:26:26 -07:00
$scope . incomingEmail = {
busy : false ,
2019-05-09 15:40:48 -07:00
setupDns : true ,
setupDnsBusy : false ,
2018-01-22 13:01:38 -08:00
2018-07-30 11:26:26 -07:00
toggleEmailEnabled : function ( ) {
2020-02-11 21:06:34 +01:00
if ( $scope . domain . mailConfig . enabled ) {
2018-07-30 11:26:26 -07:00
$ ( '#disableEmailModal' ) . modal ( 'show' ) ;
} else {
$ ( '#enableEmailModal' ) . modal ( 'show' ) ;
}
} ,
2018-05-29 17:17:53 +02:00
2019-05-09 15:40:48 -07:00
setDnsRecords : function ( callback ) {
$scope . incomingEmail . setupDnsBusy = true ;
2021-02-24 22:00:05 -08:00
Client . setDnsRecords ( { domain : $scope . domain . domain , type : 'mail' } , function ( error ) {
2019-05-09 15:40:48 -07:00
if ( error ) console . error ( error ) ;
$timeout ( function ( ) { $scope . incomingEmail . setupDnsBusy = false ; } , 2000 ) ; // otherwise, it's too fast
if ( callback ) callback ( ) ;
} ) ;
} ,
2018-07-30 11:26:26 -07:00
enable : function ( ) {
$ ( '#enableEmailModal' ) . modal ( 'hide' ) ;
2018-01-22 13:01:38 -08:00
2018-07-30 11:26:26 -07:00
$scope . incomingEmail . busy = true ;
2018-07-25 10:51:58 -07:00
2020-02-11 21:06:34 +01:00
Client . enableMailForDomain ( $scope . domain . domain , true , function ( error ) {
2018-07-25 10:51:58 -07:00
if ( error ) return console . error ( error ) ;
2018-08-12 13:13:52 -07:00
$scope . reconfigureEmailApps ( ) ;
2019-05-09 15:40:48 -07:00
let maybeSetupDns = $scope . incomingEmail . setupDns ? $scope . incomingEmail . setDnsRecords : function ( next ) { next ( ) ; } ;
maybeSetupDns ( function ( error ) {
2018-07-30 11:26:26 -07:00
if ( error ) return console . error ( error ) ;
2020-05-26 17:01:38 -07:00
$timeout ( function ( ) {
$scope . refreshDomain ( ) ;
$scope . incomingEmail . busy = false ;
} , 5000 ) ; // wait for mail container to restart. it cannot get IP otherwise while refreshing
2018-07-30 11:26:26 -07:00
} ) ;
2018-07-25 10:51:58 -07:00
} ) ;
2018-07-30 11:26:26 -07:00
} ,
2018-01-22 13:01:38 -08:00
2018-07-30 11:26:26 -07:00
disable : function ( ) {
$ ( '#disableEmailModal' ) . modal ( 'hide' ) ;
2018-05-06 23:36:40 -07:00
2018-07-30 11:26:26 -07:00
$scope . incomingEmail . busy = true ;
2020-02-11 21:06:34 +01:00
Client . enableMailForDomain ( $scope . domain . domain , false , function ( error ) {
2018-07-30 11:26:26 -07:00
if ( error ) return console . error ( error ) ;
2018-08-12 13:13:52 -07:00
$scope . reconfigureEmailApps ( ) ;
2020-05-26 17:01:38 -07:00
$timeout ( function ( ) {
$scope . refreshDomain ( ) ;
$scope . incomingEmail . busy = false ;
} , 5000 ) ; // wait for mail container to restart. it cannot get IP otherwise while refreshing
2018-07-30 11:26:26 -07:00
} ) ;
}
2018-01-22 13:01:38 -08:00
} ;
2018-03-30 18:06:40 +02:00
$scope . mailboxes = {
2018-04-05 21:00:33 +02:00
mailboxes : [ ] ,
2019-10-22 12:47:32 +02:00
search : '' ,
2020-07-05 12:18:34 -07:00
currentPage : 1 ,
perPage : 10 ,
2018-04-06 16:51:57 +02:00
2018-04-05 21:00:33 +02:00
add : {
2018-04-09 15:56:20 +02:00
error : null ,
2018-04-05 21:00:33 +02:00
busy : false ,
name : '' ,
owner : null ,
2018-04-09 15:56:20 +02:00
reset : function ( ) {
$scope . mailboxes . add . busy = false ;
$scope . mailboxes . add . error = null ;
$scope . mailboxes . add . name = '' ;
$scope . mailboxes . add . owner = null ;
} ,
show : function ( ) {
$scope . mailboxes . add . reset ( ) ;
$ ( '#mailboxAddModal' ) . modal ( 'show' ) ;
} ,
2018-04-05 21:00:33 +02:00
submit : function ( ) {
$scope . mailboxes . add . busy = true ;
2020-11-12 23:53:02 -08:00
Client . addMailbox ( $scope . domain . domain , $scope . mailboxes . add . name , $scope . mailboxes . add . owner . id , $scope . mailboxes . add . owner . type , function ( error ) {
2018-04-06 16:51:57 +02:00
if ( error ) {
2018-04-09 15:56:20 +02:00
$scope . mailboxes . add . busy = false ;
2018-04-09 16:21:32 +02:00
$scope . mailboxes . add . error = error ;
2018-04-06 16:51:57 +02:00
return ;
}
2020-03-28 17:48:11 -07:00
$scope . mailboxes . refresh ( ) ;
$scope . catchall . refresh ( ) ;
2018-04-12 13:02:32 +02:00
2020-03-28 17:48:11 -07:00
$ ( '#mailboxAddModal' ) . modal ( 'hide' ) ;
2018-04-05 21:00:33 +02:00
} ) ;
}
} ,
2018-04-09 12:42:14 +02:00
edit : {
busy : false ,
error : null ,
2018-04-12 12:26:51 +02:00
name : '' ,
owner : null ,
2020-04-19 19:44:45 -07:00
aliases : [ ] ,
2022-02-25 13:05:18 +01:00
aliasesOriginal : [ ] ,
2021-04-14 22:37:59 -07:00
active : true ,
2021-10-08 10:20:17 -07:00
enablePop3 : false ,
2022-08-18 09:11:15 +02:00
storageQuota : 0 ,
storageQuotaEnabled : false ,
2020-04-19 19:44:45 -07:00
addAlias : function ( event ) {
event . preventDefault ( ) ;
$scope . mailboxes . edit . aliases . push ( {
name : '' ,
2021-12-15 16:06:22 +01:00
domain : domainName ,
reversedSortingNotation : 'z' . repeat ( 100 ) // quick and dirty to ensure newly added are on bottom
2020-04-19 19:44:45 -07:00
} ) ;
} ,
2022-02-23 17:02:45 +01:00
delAlias : function ( event , alias ) {
2020-04-19 19:44:45 -07:00
event . preventDefault ( ) ;
2022-02-23 17:02:45 +01:00
var index = $scope . mailboxes . edit . aliases . findIndex ( function ( a ) { return ( a . name + a . domain ) === ( alias . name + alias . domain ) ; } ) ;
if ( index === - 1 ) return ;
2020-04-19 19:44:45 -07:00
$scope . mailboxes . edit . aliases . splice ( index , 1 ) ;
} ,
2018-04-09 12:42:14 +02:00
show : function ( mailbox ) {
2018-04-12 12:26:51 +02:00
$scope . mailboxes . edit . name = mailbox . name ;
2019-01-10 13:31:49 -08:00
$scope . mailboxes . edit . owner = mailbox . owner ; // this can be null if mailbox had no owner
2021-12-15 16:06:22 +01:00
$scope . mailboxes . edit . aliases = angular . copy ( mailbox . aliases , [ ] ) . map ( function ( a ) { a . reversedSortingNotation = a . domain + '@' + a . name ; return a ; } ) ;
2021-04-14 22:37:59 -07:00
$scope . mailboxes . edit . active = mailbox . active ;
2021-10-08 10:20:17 -07:00
$scope . mailboxes . edit . enablePop3 = mailbox . enablePop3 ;
2022-08-18 09:11:15 +02:00
$scope . mailboxes . edit . storageQuotaEnabled = ! ! mailbox . storageQuota ;
$scope . mailboxes . edit . storageQuota = mailbox . storageQuota || $scope . storageQuotaTicks [ 0 ] ;
2018-04-09 12:42:14 +02:00
2022-02-25 13:05:18 +01:00
// make a copy for later comparison
angular . copy ( $scope . mailboxes . edit . aliases , $scope . mailboxes . edit . aliasesOriginal ) ;
2018-04-09 12:42:14 +02:00
$ ( '#mailboxEditModal' ) . modal ( 'show' ) ;
} ,
submit : function ( ) {
2018-04-09 15:01:12 +02:00
$scope . mailboxes . edit . busy = true ;
2018-04-09 12:42:14 +02:00
2021-10-08 10:20:17 -07:00
var data = {
ownerId : $scope . mailboxes . edit . owner . id ,
ownerType : $scope . mailboxes . edit . owner . type ,
active : $scope . mailboxes . edit . active ,
2022-08-18 09:11:15 +02:00
enablePop3 : $scope . mailboxes . edit . enablePop3 ,
2024-03-12 15:43:08 +01:00
storageQuota : $scope . mailboxes . edit . storageQuotaEnabled ? parseInt ( $scope . mailboxes . edit . storageQuota ) : 0 ,
2022-08-18 09:11:15 +02:00
messagesQuota : 0
2021-10-08 10:20:17 -07:00
} ;
2019-01-10 13:31:49 -08:00
// $scope.mailboxes.edit.owner is expected to be validated by the UI
2021-10-08 10:20:17 -07:00
Client . updateMailbox ( $scope . domain . domain , $scope . mailboxes . edit . name , data , function ( error ) {
2018-04-09 12:42:14 +02:00
if ( error ) {
$scope . mailboxes . edit . error = error ;
$scope . mailboxes . edit . busy = false ;
return ;
}
2022-02-25 13:05:18 +01:00
function done ( ) {
2023-06-15 20:15:33 +05:30
updateMailUsage ( $scope . mailboxes . edit . name + '@' + $scope . domain . domain , $scope . mailboxes . edit . storageQuotaEnabled ? $scope . mailboxes . edit . storageQuota : 0 ) ; // hack to avoid refresh
2022-08-21 09:40:14 +02:00
2018-04-09 12:42:14 +02:00
$scope . mailboxes . edit . busy = false ;
$scope . mailboxes . edit . error = null ;
2018-04-12 12:26:51 +02:00
$scope . mailboxes . edit . name = '' ;
$scope . mailboxes . edit . owner = null ;
2020-04-19 19:44:45 -07:00
$scope . mailboxes . edit . aliases = [ ] ;
2022-08-21 09:40:14 +02:00
$scope . mailboxes . edit . storageQuota = 0 ;
$scope . mailboxes . edit . storageQuotaEnabled = false ;
2018-04-09 12:42:14 +02:00
$scope . mailboxes . refresh ( ) ;
$ ( '#mailboxEditModal' ) . modal ( 'hide' ) ;
2022-02-25 13:05:18 +01:00
}
// skip if nothing has changed
if ( angular . equals ( $scope . mailboxes . edit . aliases , $scope . mailboxes . edit . aliasesOriginal ) ) return done ( ) ;
Client . setAliases ( $scope . mailboxes . edit . name , $scope . domain . domain , $scope . mailboxes . edit . aliases , function ( error ) {
if ( error ) {
$scope . mailboxes . edit . error = error ;
$scope . mailboxes . edit . busy = false ;
return ;
}
done ( ) ;
2018-04-09 12:42:14 +02:00
} ) ;
} ) ;
}
} ,
2018-04-05 21:22:07 +02:00
remove : {
busy : false ,
mailbox : null ,
2020-07-27 22:36:38 -07:00
deleteMails : true ,
2018-04-05 21:22:07 +02:00
show : function ( mailbox ) {
$scope . mailboxes . remove . mailbox = mailbox ;
$ ( '#mailboxRemoveModal' ) . modal ( 'show' ) ;
} ,
submit : function ( ) {
$scope . mailboxes . remove . busy = true ;
2020-07-27 22:36:38 -07:00
Client . removeMailbox ( $scope . domain . domain , $scope . mailboxes . remove . mailbox . name , $scope . mailboxes . remove . deleteMails , function ( error ) {
2018-04-05 21:22:07 +02:00
$scope . mailboxes . remove . busy = false ;
if ( error ) return console . error ( error ) ;
$scope . mailboxes . remove . mailbox = null ;
2018-04-12 13:02:32 +02:00
$scope . mailboxes . refresh ( function ( error ) {
if ( error ) return console . error ( error ) ;
$scope . catchall . refresh ( ) ;
2018-04-05 21:22:07 +02:00
2018-04-12 13:02:32 +02:00
$ ( '#mailboxRemoveModal' ) . modal ( 'hide' ) ;
} ) ;
2018-04-05 21:22:07 +02:00
} ) ;
}
} ,
2018-04-12 13:02:32 +02:00
refresh : function ( callback ) {
callback = typeof callback === 'function' ? callback : function ( error ) { if ( error ) return console . error ( error ) ; } ;
2021-01-07 21:40:51 -08:00
Client . listMailboxes ( $scope . domain . domain , $scope . mailboxes . search , $scope . mailboxes . currentPage , $scope . mailboxes . perPage , function ( error , mailboxes ) {
2018-04-12 13:02:32 +02:00
if ( error ) return callback ( error ) ;
2018-04-05 21:00:33 +02:00
2021-01-07 21:40:51 -08:00
mailboxes . forEach ( function ( m ) {
2023-01-24 18:46:42 +01:00
m . fullName = m . name + '@' + m . domain ; // to make it simple for the ui
2021-01-07 21:40:51 -08:00
m . owner = $scope . owners . find ( function ( o ) { return o . id === m . ownerId ; } ) ; // owner may not exist
m . ownerDisplayName = m . owner ? m . owner . display : '' ; // this meta property is set when we get the user list
} ) ;
2018-04-12 13:02:32 +02:00
2021-01-07 21:40:51 -08:00
$scope . mailboxes . mailboxes = mailboxes ;
2020-07-05 12:18:34 -07:00
2021-01-07 21:40:51 -08:00
callback ( ) ;
2018-04-05 21:00:33 +02:00
} ) ;
2020-07-05 12:18:34 -07:00
} ,
showNextPage : function ( ) {
$scope . mailboxes . currentPage ++ ;
$scope . mailboxes . refresh ( ) ;
} ,
showPrevPage : function ( ) {
if ( $scope . mailboxes . currentPage > 1 ) $scope . mailboxes . currentPage -- ;
else $scope . mailboxes . currentPage = 1 ;
$scope . mailboxes . refresh ( ) ;
} ,
updateFilter : function ( fresh ) {
if ( fresh ) $scope . mailboxes . currentPage = 1 ;
$scope . mailboxes . refresh ( ) ;
2018-03-30 18:06:40 +02:00
}
} ;
2018-01-22 13:01:38 -08:00
$scope . mailRelayPresets = [
{ provider : 'cloudron-smtp' , name : 'Built-in SMTP server' } ,
{ provider : 'external-smtp' , name : 'External SMTP server' , host : '' , port : 587 } ,
2019-04-22 16:49:59 -07:00
{ provider : 'external-smtp-noauth' , name : 'External SMTP server (No Authentication)' , host : '' , port : 587 } ,
2019-08-02 12:42:10 -07:00
{ provider : 'ses-smtp' , name : 'Amazon SES' , host : 'email-smtp.us-east-1.amazonaws.com' , port : 587 , spfDoc : 'https://docs.aws.amazon.com/ses/latest/DeveloperGuide/spf.html' } ,
2020-11-19 11:12:46 -08:00
{ provider : 'elasticemail-smtp' , name : 'Elastic Email' , host : 'smtp.elasticemail.com' , port : 587 , spfDoc : 'https://elasticemail.com/blog/marketing_tips/common-spf-errors' } ,
2019-08-02 12:42:10 -07:00
{ provider : 'google-smtp' , name : 'Google' , host : 'smtp.gmail.com' , port : 587 , spfDoc : 'https://support.google.com/a/answer/33786?hl=en' } ,
{ provider : 'mailgun-smtp' , name : 'Mailgun' , host : 'smtp.mailgun.org' , port : 587 , spfDoc : 'https://www.mailgun.com/blog/white-labeling-dns-records-your-customers-tips-tricks' } ,
{ provider : 'mailjet-smtp' , name : 'Mailjet' , host : '' , port : 587 , spfDoc : 'https://app.mailjet.com/docs/spf-dkim-guide' } ,
2022-10-27 23:13:57 +02:00
{ provider : 'office365-legacy-smtp' , name : 'Office 365' , host : 'smtp-legacy.office365.com' , port : 587 } , // uses "login" AUTH instead of "plain"
2019-08-02 12:42:10 -07:00
{ provider : 'postmark-smtp' , name : 'Postmark' , host : 'smtp.postmarkapp.com' , port : 587 , spfDoc : 'https://postmarkapp.com/support/article/1092-how-do-i-set-up-spf-for-postmark' } ,
{ provider : 'sendgrid-smtp' , name : 'SendGrid' , host : 'smtp.sendgrid.net' , port : 587 , username : 'apikey' , spfDoc : 'https://sendgrid.com/docs/ui/account-and-settings/spf-records/' } ,
{ provider : 'sparkpost-smtp' , name : 'SparkPost' , host : 'smtp.sparkpostmail.com' , port : 587 , username : 'SMTP_Injection' , spfDoc : 'https://www.sparkpost.com/resources/email-explained/spf-sender-policy-framework/' } ,
2019-03-15 11:16:43 -07:00
{ provider : 'noop' , name : 'Disable' } ,
2018-01-22 13:01:38 -08:00
] ;
2019-04-22 16:49:59 -07:00
$scope . usesTokenAuth = function ( provider ) {
2019-07-15 10:49:13 -07:00
return provider === 'postmark-smtp' || provider === 'sendgrid-smtp' || provider === 'sparkpost-smtp' ;
2019-04-22 16:49:59 -07:00
} ;
$scope . usesExternalServer = function ( provider ) {
return provider !== 'cloudron-smtp' && provider !== 'noop' ;
} ;
$scope . usesPasswordAuth = function ( provider ) {
return provider === 'external-smtp'
|| provider === 'ses-smtp'
|| provider === 'google-smtp'
|| provider === 'mailgun-smtp'
2020-11-19 11:12:46 -08:00
|| provider === 'elasticemail-smtp'
2022-10-27 23:13:57 +02:00
|| provider === 'office365-legacy-smtp'
2019-04-22 16:49:59 -07:00
|| provider === 'mailjet-smtp' ;
} ;
2018-01-22 13:01:38 -08:00
$scope . mailRelay = {
error : null ,
success : false ,
busy : false ,
preset : $scope . mailRelayPresets [ 0 ] ,
2020-08-24 10:16:11 -07:00
// form data to be set on load
relay : {
provider : 'cloudron-smtp' ,
host : '' ,
port : 25 ,
username : '' ,
password : '' ,
serverApiToken : '' ,
acceptSelfSignedCerts : false
} ,
2018-01-22 13:01:38 -08:00
presetChanged : function ( ) {
$scope . mailRelay . error = null ;
$scope . mailRelay . relay . provider = $scope . mailRelay . preset . provider ;
$scope . mailRelay . relay . host = $scope . mailRelay . preset . host ;
$scope . mailRelay . relay . port = $scope . mailRelay . preset . port ;
$scope . mailRelay . relay . username = '' ;
$scope . mailRelay . relay . password = '' ;
$scope . mailRelay . relay . serverApiToken = '' ;
2019-04-23 15:24:31 -07:00
$scope . mailRelay . relay . acceptSelfSignedCerts = false ;
2018-01-22 13:01:38 -08:00
} ,
submit : function ( ) {
$scope . mailRelay . error = null ;
$scope . mailRelay . busy = true ;
$scope . mailRelay . success = false ;
var data = {
provider : $scope . mailRelay . relay . provider ,
host : $scope . mailRelay . relay . host ,
2019-04-23 15:24:31 -07:00
port : $scope . mailRelay . relay . port ,
2021-10-16 21:47:28 -07:00
acceptSelfSignedCerts : $scope . mailRelay . relay . acceptSelfSignedCerts ,
forceFromAddress : false
2018-01-22 13:01:38 -08:00
} ;
// fill in provider specific username/password usage
if ( data . provider === 'postmark-smtp' ) {
data . username = $scope . mailRelay . relay . serverApiToken ;
data . password = $scope . mailRelay . relay . serverApiToken ;
2021-10-16 21:47:28 -07:00
data . forceFromAddress = true ; // postmark requires the "From:" in mail to be a Sender Signature
2018-01-22 13:01:38 -08:00
} else if ( data . provider === 'sendgrid-smtp' ) {
data . username = 'apikey' ;
data . password = $scope . mailRelay . relay . serverApiToken ;
2019-07-15 10:49:13 -07:00
} else if ( data . provider === 'sparkpost-smtp' ) {
data . username = 'SMTP_Injection' ;
data . password = $scope . mailRelay . relay . serverApiToken ;
2018-01-22 13:01:38 -08:00
} else {
data . username = $scope . mailRelay . relay . username ;
data . password = $scope . mailRelay . relay . password ;
}
2020-02-11 21:06:34 +01:00
Client . setMailRelay ( $scope . domain . domain , data , function ( error ) {
2018-01-22 13:01:38 -08:00
if ( error ) {
$scope . mailRelay . error = error . message ;
2020-02-27 10:42:30 -08:00
$scope . mailRelay . busy = false ;
2018-01-22 13:01:38 -08:00
return ;
}
2020-02-11 21:06:34 +01:00
$scope . domain . relay = data ;
2018-01-23 12:30:35 +01:00
2020-02-27 10:42:30 -08:00
// let the mail server restart, otherwise we get "Error getting IP" errors during refresh
$timeout ( function ( ) {
$scope . mailRelay . busy = false ;
$scope . mailRelay . success = true ;
$scope . refreshDomain ( ) ;
// clear success indicator after 5sec
$timeout ( function ( ) { $scope . mailRelay . success = false ; } , 5000 ) ;
} , 5000 ) ;
2018-01-22 13:01:38 -08:00
} ) ;
}
} ;
2018-05-06 23:47:38 -07:00
function resetDnsRecords ( ) {
2018-01-23 12:30:35 +01:00
$scope . expectedDnsRecordsTypes . forEach ( function ( record ) {
var type = record . value ;
2018-05-06 23:47:38 -07:00
$scope . expectedDnsRecords [ type ] = { } ;
2018-01-23 12:30:35 +01:00
$ ( '#collapse_dns_' + type ) . collapse ( 'hide' ) ;
} ) ;
$ ( '#collapse_outbound_smtp' ) . collapse ( 'hide' ) ;
$ ( '#collapse_rbl' ) . collapse ( 'hide' ) ;
}
function showExpectedDnsRecords ( ) {
// open the record details if they are not correct
$scope . expectedDnsRecordsTypes . forEach ( function ( record ) {
var type = record . value ;
2020-02-11 21:06:34 +01:00
$scope . expectedDnsRecords [ type ] = $scope . domain . mailStatus . dns [ type ] || { } ;
2018-01-22 13:01:38 -08:00
2018-01-23 12:30:35 +01:00
if ( ! $scope . expectedDnsRecords [ type ] . status ) {
$ ( '#collapse_dns_' + type ) . collapse ( 'show' ) ;
}
2018-01-22 13:01:38 -08:00
} ) ;
2018-01-23 12:30:35 +01:00
2020-02-11 21:06:34 +01:00
if ( ! $scope . domain . mailStatus . relay . status ) {
2018-01-23 12:30:35 +01:00
$ ( '#collapse_outbound_smtp' ) . collapse ( 'show' ) ;
}
2020-02-11 21:06:34 +01:00
if ( ! $scope . domain . mailStatus . rbl . status ) {
2018-01-23 12:30:35 +01:00
$ ( '#collapse_rbl' ) . collapse ( 'show' ) ;
}
2018-01-22 13:01:38 -08:00
}
2018-04-09 18:00:08 +02:00
$scope . selectDomain = function ( ) {
2020-02-11 21:06:34 +01:00
$location . path ( '/email/' + $scope . domain . domain , false ) ;
2018-04-09 18:00:08 +02:00
} ;
2020-09-08 19:34:27 -07:00
// this is required because we need to rewrite the CLOUDRON_MAIL_SERVER_HOST env var
2018-08-12 13:13:52 -07:00
$scope . reconfigureEmailApps = function ( ) {
var installedApps = Client . getInstalledApps ( ) ;
for ( var i = 0 ; i < installedApps . length ; i ++ ) {
if ( ! installedApps [ i ] . manifest . addons . email ) continue ;
2019-10-04 11:20:27 -07:00
Client . repairApp ( installedApps [ i ] . id , { } , function ( error ) {
2018-08-12 13:13:52 -07:00
if ( error ) console . error ( error ) ;
} ) ;
}
} ;
2018-01-23 12:30:35 +01:00
$scope . refreshDomain = function ( ) {
$scope . refreshBusy = true ;
2018-01-22 13:01:38 -08:00
2018-05-06 23:47:38 -07:00
resetDnsRecords ( ) ;
2018-01-23 12:30:35 +01:00
2020-02-11 21:06:34 +01:00
Client . getMailConfigForDomain ( domainName , function ( error , mailConfig ) {
2022-02-17 17:54:44 +01:00
if ( error && error . statusCode === 404 ) return $location . path ( '/email' ) ;
2018-01-23 12:30:35 +01:00
if ( error ) {
$scope . refreshBusy = false ;
return console . error ( error ) ;
}
// pre-fill the form
$scope . mailRelay . relay . provider = mailConfig . relay . provider ;
$scope . mailRelay . relay . host = mailConfig . relay . host ;
$scope . mailRelay . relay . port = mailConfig . relay . port ;
2019-04-23 15:24:31 -07:00
$scope . mailRelay . relay . acceptSelfSignedCerts = ! ! mailConfig . relay . acceptSelfSignedCerts ;
2018-01-22 13:01:38 -08:00
$scope . mailRelay . relay . username = '' ;
$scope . mailRelay . relay . password = '' ;
$scope . mailRelay . relay . serverApiToken = '' ;
2018-01-23 12:30:35 +01:00
if ( mailConfig . relay . provider === 'postmark-smtp' ) {
$scope . mailRelay . relay . serverApiToken = mailConfig . relay . username ;
} else if ( mailConfig . relay . provider === 'sendgrid-smtp' ) {
$scope . mailRelay . relay . serverApiToken = mailConfig . relay . password ;
2019-07-15 10:49:13 -07:00
} else if ( mailConfig . relay . provider === 'sparkpost-smtp' ) {
$scope . mailRelay . relay . serverApiToken = mailConfig . relay . password ;
2018-01-22 13:01:38 -08:00
} else {
2018-01-23 12:30:35 +01:00
$scope . mailRelay . relay . username = mailConfig . relay . username ;
$scope . mailRelay . relay . password = mailConfig . relay . password ;
2018-01-22 13:01:38 -08:00
}
for ( var i = 0 ; i < $scope . mailRelayPresets . length ; i ++ ) {
2018-01-23 12:30:35 +01:00
if ( $scope . mailRelayPresets [ i ] . provider === mailConfig . relay . provider ) {
2018-01-22 13:01:38 -08:00
$scope . mailRelay . preset = $scope . mailRelayPresets [ i ] ;
break ;
}
}
2020-08-24 10:16:11 -07:00
$scope . banner . text = mailConfig . banner . text || '' ;
$scope . banner . html = mailConfig . banner . html || '' ;
2018-01-23 12:41:52 +01:00
// amend to selected domain to be available for the UI
2020-02-11 21:06:34 +01:00
$scope . domain . mailConfig = mailConfig ;
$scope . domain . mailStatus = { } ;
2018-01-23 12:41:52 +01:00
2023-01-24 18:46:42 +01:00
refreshMailUsage ( ) ;
$scope . mailboxes . refresh ( ) ;
$scope . mailinglists . refresh ( ) ;
$scope . catchall . refresh ( ) ;
2018-04-01 21:58:12 +02:00
2018-01-23 12:41:52 +01:00
// we will fetch the status without blocking the ui
2020-02-11 21:06:34 +01:00
Client . getMailStatusForDomain ( $scope . domain . domain , function ( error , mailStatus ) {
2018-01-23 12:41:52 +01:00
$scope . refreshBusy = false ;
2018-01-22 13:01:38 -08:00
2018-01-23 12:41:52 +01:00
if ( error ) return console . error ( error ) ;
2018-01-22 13:01:38 -08:00
2020-02-11 21:06:34 +01:00
$scope . domain . mailStatus = mailStatus ;
2018-01-22 13:01:38 -08:00
2018-01-23 12:30:35 +01:00
showExpectedDnsRecords ( ) ;
} ) ;
2018-01-22 13:01:38 -08:00
} ) ;
2018-01-23 12:30:35 +01:00
} ;
2018-01-22 13:01:38 -08:00
2018-01-23 12:38:19 +01:00
$scope . refreshStatus = function ( ) {
$scope . refreshBusy = true ;
2020-02-11 21:06:34 +01:00
Client . getMailStatusForDomain ( $scope . domain . domain , function ( error , mailStatus ) {
2018-01-23 12:38:19 +01:00
if ( error ) {
$scope . refreshBusy = false ;
return console . error ( error ) ;
}
// overwrite the selected domain status to be available for the UI
2020-02-11 21:06:34 +01:00
$scope . domain . mailStatus = mailStatus ;
2018-01-23 12:38:19 +01:00
showExpectedDnsRecords ( ) ;
$scope . refreshBusy = false ;
} ) ;
} ;
2021-12-02 14:49:35 -08:00
$scope . howToConnectInfo = {
show : function ( ) {
$ ( '#howToConnectInfoModal' ) . modal ( 'show' ) ;
}
} ;
2018-01-23 15:11:33 +01:00
Client . onReady ( function ( ) {
2020-02-11 21:06:34 +01:00
$scope . isAdminDomain = $scope . config . adminDomain === domainName ;
2018-04-09 18:00:08 +02:00
2022-02-14 14:55:04 +01:00
Client . getAllUsers ( function ( error , users ) {
2018-01-23 15:11:33 +01:00
if ( error ) return console . error ( 'Unable to get user listing.' , error ) ;
2018-01-22 13:01:38 -08:00
2018-04-05 21:00:33 +02:00
// ensure we have a display value available
2020-11-13 13:21:01 -08:00
$scope . owners . push ( { header : true , display : $translate . instant ( 'email.mailboxboxDialog.usersHeader' ) } ) ;
2020-11-12 23:53:02 -08:00
users . forEach ( function ( u ) {
$scope . owners . push ( { display : u . username || u . email , id : u . id , type : 'user' } ) ;
2018-04-05 21:00:33 +02:00
} ) ;
2018-01-22 13:01:38 -08:00
2020-11-12 23:53:02 -08:00
Client . getGroups ( function ( error , groups ) {
if ( error ) return console . error ( 'Unable to get group listing.' , error ) ;
2018-12-06 10:28:42 -08:00
2020-11-13 13:21:01 -08:00
$scope . owners . push ( { header : true , display : $translate . instant ( 'email.mailboxboxDialog.groupsHeader' ) } ) ;
2020-11-12 23:53:02 -08:00
groups . forEach ( function ( g ) {
$scope . owners . push ( { display : g . name , id : g . id , type : 'group' } ) ;
} ) ;
2018-01-22 13:01:38 -08:00
2021-12-02 22:21:25 -08:00
$scope . owners . push ( { header : true , display : $translate . instant ( 'email.mailboxboxDialog.appsHeader' ) } ) ;
Client . getInstalledApps ( ) . forEach ( function ( a ) {
if ( a . manifest . addons && a . manifest . addons . recvmail ) $scope . owners . push ( { display : a . label || a . fqdn , id : a . id , type : 'app' } ) ;
} ) ;
2020-11-12 23:53:02 -08:00
Client . getDomains ( function ( error , result ) {
2022-02-17 17:54:44 +01:00
if ( error ) return console . error ( 'Unable to list domains.' , error ) ;
2018-04-09 18:00:08 +02:00
2020-11-12 23:53:02 -08:00
$scope . domain = result . filter ( function ( d ) { return d . domain === domainName ; } ) [ 0 ] ;
$scope . adminDomain = result . filter ( function ( d ) { return d . domain === $scope . config . adminDomain ; } ) [ 0 ] ;
2018-04-09 18:00:08 +02:00
2020-11-12 23:53:02 -08:00
async . eachSeries ( result , function ( domain , iteratorDone ) {
Client . getMailConfigForDomain ( domain . domain , function ( error , mailConfig ) {
if ( error ) return console . error ( 'Failed to fetch mail config for domain' , domain . domain , error ) ;
if ( mailConfig . enabled ) $scope . incomingDomains . push ( domain ) ;
iteratorDone ( ) ;
} ) ;
} , function iteratorDone ( error ) {
if ( error ) return console . error ( error ) ;
2018-04-09 18:00:08 +02:00
2022-09-28 22:10:43 +02:00
$scope . refreshDomain ( ) ; // this calls catchall.refresh() which in turn relies on incomingDomains
2021-11-19 15:45:16 +01:00
$scope . setView ( $routeParams . view || 'mailboxes' , true /* always set */ ) ;
2020-11-12 23:53:02 -08:00
$scope . ready = true ;
} ) ;
2020-02-11 21:06:34 +01:00
} ) ;
} ) ;
2018-04-09 18:00:08 +02:00
} ) ;
2018-01-22 13:01:38 -08:00
} ) ;
// setup all the dialog focus handling
2020-02-12 14:51:06 +01:00
[ 'mailboxAddModal' , 'mailboxEditModal' , 'mailinglistEditModal' , 'mailinglistAddModal' ] . forEach ( function ( id ) {
2018-01-22 13:01:38 -08:00
$ ( '#' + id ) . on ( 'shown.bs.modal' , function ( ) {
2019-09-11 14:09:53 -07:00
$ ( this ) . find ( '[autofocus]:first' ) . focus ( ) ;
2018-01-22 13:01:38 -08:00
} ) ;
} ) ;
$ ( '.modal-backdrop' ) . remove ( ) ;
} ] ) ;