View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2009  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  package davmail;
20  
21  import davmail.ui.tray.DavGatewayTray;
22  import org.apache.log4j.*;
23  
24  import java.io.*;
25  import java.nio.charset.StandardCharsets;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.Paths;
29  import java.nio.file.attribute.FileAttribute;
30  import java.nio.file.attribute.PosixFilePermissions;
31  import java.util.*;
32  
33  import static org.apache.http.util.TextUtils.isEmpty;
34  
35  /**
36   * Settings facade.
37   * DavMail settings are stored in the .davmail.properties file in current
38   * user home directory or in the file specified on the command line.
39   */
40  public final class Settings {
41  
42      private static final Logger LOGGER = Logger.getLogger(Settings.class);
43  
44      public static final String OUTLOOK_URL = "https://outlook.office365.com";
45      public static final String O365_URL = OUTLOOK_URL+"/EWS/Exchange.asmx";
46  
47      public static final String GRAPH_URL = "https://graph.microsoft.com";
48  
49      public static final String O365_LOGIN_URL = "https://login.microsoftonline.com";
50  
51      public static final String O365 = "O365";
52      public static final String O365_MODERN = "O365Modern";
53      public static final String O365_INTERACTIVE = "O365Interactive";
54      public static final String O365_MANUAL = "O365Manual";
55      public static final String WEBDAV = "WebDav";
56      public static final String EWS = "EWS";
57      public static final String AUTO = "Auto";
58  
59      public static final String EDGE_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36 Edg/90.0.818.49";
60  
61      private Settings() {
62      }
63  
64      private static final Properties SETTINGS_PROPERTIES = new Properties() {
65          @Override
66          public synchronized Enumeration<Object> keys() {
67              Enumeration<Object> keysEnumeration = super.keys();
68              TreeSet<String> sortedKeySet = new TreeSet<>();
69              while (keysEnumeration.hasMoreElements()) {
70                  sortedKeySet.add((String) keysEnumeration.nextElement());
71              }
72              final Iterator<String> sortedKeysIterator = sortedKeySet.iterator();
73              return new Enumeration<Object>() {
74  
75                  public boolean hasMoreElements() {
76                      return sortedKeysIterator.hasNext();
77                  }
78  
79                  public Object nextElement() {
80                      return sortedKeysIterator.next();
81                  }
82              };
83          }
84  
85      };
86      private static String configFilePath;
87      private static boolean isFirstStart;
88  
89      /**
90       * Set config file path (from command line parameter).
91       *
92       * @param path davmail properties file path
93       */
94      public static synchronized void setConfigFilePath(String path) {
95          configFilePath = path;
96      }
97  
98      /**
99       * Detect first launch (properties file does not exist).
100      *
101      * @return true if this is the first start with the current file path
102      */
103     public static synchronized boolean isFirstStart() {
104         return isFirstStart;
105     }
106 
107     /**
108      * Load properties from provided stream (used in webapp mode).
109      *
110      * @param inputStream properties stream
111      * @throws IOException on error
112      */
113     public static synchronized void load(InputStream inputStream) throws IOException {
114         SETTINGS_PROPERTIES.load(inputStream);
115         updateLoggingConfig();
116     }
117 
118     /**
119      * Load properties from current file path (command line or default).
120      */
121     public static synchronized void load() {
122         try {
123             if (configFilePath == null) {
124                 //noinspection AccessOfSystemProperties
125                 configFilePath = System.getProperty("user.home") + "/.davmail.properties";
126             }
127             File configFile = new File(configFilePath);
128             if (configFile.exists()) {
129                 try (FileInputStream fileInputStream = new FileInputStream(configFile)) {
130                     load(fileInputStream);
131                 }
132             } else {
133                 isFirstStart = true;
134 
135                 // first start : set default values, ports above 1024 for unix/linux
136                 setDefaultSettings();
137                 save();
138             }
139         } catch (IOException e) {
140             DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_LOAD_SETTINGS"), e);
141         }
142         updateLoggingConfig();
143     }
144 
145     /**
146      * Set all settings to default values.
147      * Ports above 1024 for unix/linux
148      */
149     public static void setDefaultSettings() {
150         SETTINGS_PROPERTIES.put("davmail.mode", "EWS");
151         SETTINGS_PROPERTIES.put("davmail.url", getO365Url());
152         SETTINGS_PROPERTIES.put("davmail.popPort", "1110");
153         SETTINGS_PROPERTIES.put("davmail.imapPort", "1143");
154         SETTINGS_PROPERTIES.put("davmail.smtpPort", "1025");
155         SETTINGS_PROPERTIES.put("davmail.caldavPort", "1080");
156         SETTINGS_PROPERTIES.put("davmail.ldapPort", "1389");
157         SETTINGS_PROPERTIES.put("davmail.clientSoTimeout", "");
158         SETTINGS_PROPERTIES.put("davmail.keepDelay", "30");
159         SETTINGS_PROPERTIES.put("davmail.sentKeepDelay", "0");
160         SETTINGS_PROPERTIES.put("davmail.caldavPastDelay", "0");
161         SETTINGS_PROPERTIES.put("davmail.caldavAutoSchedule", Boolean.TRUE.toString());
162         SETTINGS_PROPERTIES.put("davmail.imapIdleDelay", "");
163         SETTINGS_PROPERTIES.put("davmail.folderSizeLimit", "");
164         SETTINGS_PROPERTIES.put("davmail.enableKeepAlive", Boolean.FALSE.toString());
165         SETTINGS_PROPERTIES.put("davmail.allowRemote", Boolean.FALSE.toString());
166         SETTINGS_PROPERTIES.put("davmail.bindAddress", "");
167         SETTINGS_PROPERTIES.put("davmail.useSystemProxies", Boolean.FALSE.toString());
168         SETTINGS_PROPERTIES.put("davmail.enableProxy", Boolean.FALSE.toString());
169         SETTINGS_PROPERTIES.put("davmail.enableKerberos", "false");
170         SETTINGS_PROPERTIES.put("davmail.disableUpdateCheck", "false");
171         SETTINGS_PROPERTIES.put("davmail.proxyHost", "");
172         SETTINGS_PROPERTIES.put("davmail.proxyPort", "");
173         SETTINGS_PROPERTIES.put("davmail.proxyUser", "");
174         SETTINGS_PROPERTIES.put("davmail.proxyPassword", "");
175         SETTINGS_PROPERTIES.put("davmail.noProxyFor", "");
176         SETTINGS_PROPERTIES.put("davmail.server", Boolean.FALSE.toString());
177         SETTINGS_PROPERTIES.put("davmail.server.certificate.hash", "");
178         SETTINGS_PROPERTIES.put("davmail.caldavAlarmSound", "");
179         SETTINGS_PROPERTIES.put("davmail.carddavReadPhoto", Boolean.TRUE.toString());
180         SETTINGS_PROPERTIES.put("davmail.forceActiveSyncUpdate", Boolean.FALSE.toString());
181         SETTINGS_PROPERTIES.put("davmail.showStartupBanner", Boolean.TRUE.toString());
182         SETTINGS_PROPERTIES.put("davmail.disableGuiNotifications", Boolean.FALSE.toString());
183         SETTINGS_PROPERTIES.put("davmail.disableTrayActivitySwitch", Boolean.FALSE.toString());
184         SETTINGS_PROPERTIES.put("davmail.imapAutoExpunge", Boolean.TRUE.toString());
185         SETTINGS_PROPERTIES.put("davmail.imapAlwaysApproxMsgSize", Boolean.FALSE.toString());
186         SETTINGS_PROPERTIES.put("davmail.popMarkReadOnRetr", Boolean.FALSE.toString());
187         SETTINGS_PROPERTIES.put("davmail.smtpSaveInSent", Boolean.TRUE.toString());
188         SETTINGS_PROPERTIES.put("davmail.ssl.keystoreType", "");
189         SETTINGS_PROPERTIES.put("davmail.ssl.keystoreFile", "");
190         SETTINGS_PROPERTIES.put("davmail.ssl.keystorePass", "");
191         SETTINGS_PROPERTIES.put("davmail.ssl.keyPass", "");
192         if (isWindows()) {
193             // default to MSCAPI on windows for native client certificate access
194             SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystoreType", "MSCAPI");
195         } else {
196             SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystoreType", "");
197         }
198         SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystoreFile", "");
199         SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystorePass", "");
200         SETTINGS_PROPERTIES.put("davmail.ssl.pkcs11Library", "");
201         SETTINGS_PROPERTIES.put("davmail.ssl.pkcs11Config", "");
202         SETTINGS_PROPERTIES.put("davmail.ssl.nosecurepop", Boolean.FALSE.toString());
203         SETTINGS_PROPERTIES.put("davmail.ssl.nosecureimap", Boolean.FALSE.toString());
204         SETTINGS_PROPERTIES.put("davmail.ssl.nosecuresmtp", Boolean.FALSE.toString());
205         SETTINGS_PROPERTIES.put("davmail.ssl.nosecurecaldav", Boolean.FALSE.toString());
206         SETTINGS_PROPERTIES.put("davmail.ssl.nosecureldap", Boolean.FALSE.toString());
207 
208         // logging
209         SETTINGS_PROPERTIES.put("log4j.rootLogger", Level.WARN.toString());
210         SETTINGS_PROPERTIES.put("log4j.logger.davmail", Level.DEBUG.toString());
211         SETTINGS_PROPERTIES.put("log4j.logger.httpclient.wire", Level.WARN.toString());
212         SETTINGS_PROPERTIES.put("log4j.logger.httpclient", Level.WARN.toString());
213         SETTINGS_PROPERTIES.put("davmail.logFilePath", "");
214     }
215 
216     /**
217      * Return DavMail log file path
218      *
219      * @return full log file path
220      */
221     public static String getLogFilePath() {
222         String logFilePath = Settings.getProperty("davmail.logFilePath");
223         // set default log file path
224         if ((logFilePath == null || logFilePath.isEmpty())) {
225             if (Settings.getBooleanProperty("davmail.server")) {
226                 logFilePath = "davmail.log";
227             } else if (System.getProperty("os.name").toLowerCase().startsWith("mac os x")) {
228                 // store davmail.log in OSX Logs directory
229                 logFilePath = System.getProperty("user.home") + "/Library/Logs/DavMail/davmail.log";
230             } else {
231                 // store davmail.log in user home folder
232                 logFilePath = System.getProperty("user.home") + "/davmail.log";
233             }
234         } else {
235             File logFile = new File(logFilePath);
236             if (logFile.isDirectory()) {
237                 logFilePath += "/davmail.log";
238             }
239         }
240         return logFilePath;
241     }
242 
243     /**
244      * Return DavMail log file directory
245      *
246      * @return full log file directory
247      */
248     public static String getLogFileDirectory() {
249         String logFilePath = getLogFilePath();
250         if (logFilePath == null || logFilePath.isEmpty()) {
251             return ".";
252         }
253         int lastSlashIndex = logFilePath.lastIndexOf('/');
254         if (lastSlashIndex == -1) {
255             lastSlashIndex = logFilePath.lastIndexOf('\\');
256         }
257         if (lastSlashIndex >= 0) {
258             return logFilePath.substring(0, lastSlashIndex);
259         } else {
260             return ".";
261         }
262     }
263 
264     /**
265      * Update Log4J config from settings.
266      */
267     public static void updateLoggingConfig() {
268         String logFilePath = getLogFilePath();
269 
270         try {
271             if (logFilePath != null && !logFilePath.isEmpty()) {
272                 File logFile = new File(logFilePath);
273                 // create parent directory if needed
274                 File logFileDir = logFile.getParentFile();
275                 if (logFileDir != null && !logFileDir.exists() && (!logFileDir.mkdirs())) {
276                         DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_CREATE_LOG_FILE_DIR"));
277                         throw new IOException();
278 
279                 }
280             } else {
281                 logFilePath = "davmail.log";
282             }
283             synchronized (Logger.getRootLogger()) {
284                 // Build file appender
285                 FileAppender fileAppender = (FileAppender) Logger.getRootLogger().getAppender("FileAppender");
286                 if (fileAppender == null) {
287                     String logFileSize = Settings.getProperty("davmail.logFileSize");
288                     if (logFileSize == null || logFileSize.isEmpty()) {
289                         logFileSize = "1MB";
290                     }
291                     // set log file size to 0 to use an external rotation mechanism, e.g. logrotate
292                     if ("0".equals(logFileSize)) {
293                         fileAppender = new FileAppender();
294                     } else {
295                         fileAppender = new RollingFileAppender();
296                         ((RollingFileAppender) fileAppender).setMaxBackupIndex(2);
297                         ((RollingFileAppender) fileAppender).setMaxFileSize(logFileSize);
298                     }
299                     fileAppender.setName("FileAppender");
300                     fileAppender.setEncoding("UTF-8");
301                     fileAppender.setLayout(new PatternLayout("%d{ISO8601} %-5p [%t] %c %x - %m%n"));
302                 }
303                 fileAppender.setFile(logFilePath, true, false, 8192);
304                 Logger.getRootLogger().addAppender(fileAppender);
305             }
306 
307             // disable ConsoleAppender in gui mode
308             ConsoleAppender consoleAppender = (ConsoleAppender) Logger.getRootLogger().getAppender("ConsoleAppender");
309             if (consoleAppender != null) {
310                 if (Settings.getBooleanProperty("davmail.server")) {
311                     consoleAppender.setThreshold(Level.ALL);
312                 } else {
313                     consoleAppender.setThreshold(Level.OFF);
314                 }
315             }
316 
317         } catch (IOException e) {
318             DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_SET_LOG_FILE_PATH"));
319         }
320 
321         // update logging levels
322         Settings.setLoggingLevel("rootLogger", Settings.getLoggingLevel("rootLogger"));
323         Settings.setLoggingLevel("davmail", Settings.getLoggingLevel("davmail"));
324         // set logging levels for HttpClient 4
325         Settings.setLoggingLevel("org.apache.http.wire", Settings.getLoggingLevel("httpclient.wire"));
326         Settings.setLoggingLevel("org.apache.http.conn.ssl", Settings.getLoggingLevel("httpclient.wire"));
327         Settings.setLoggingLevel("org.apache.http", Settings.getLoggingLevel("httpclient"));
328     }
329 
330     /**
331      * Save settings in current file path (command line or default).
332      */
333     public static synchronized void save() {
334         // configFilePath is null in some test cases
335         if (configFilePath != null) {
336             // clone settings
337             Properties properties = new Properties();
338             properties.putAll(SETTINGS_PROPERTIES);
339             // file lines
340             ArrayList<String> lines = new ArrayList<>();
341 
342             // try to make .davmail.properties file readable by user only on create
343             Path path = Paths.get(configFilePath);
344             if (!Files.exists(path) && isUnix()) {
345                 FileAttribute<?> permissions = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------"));
346                 try {
347                     Files.createFile(path, permissions);
348                 } catch (IOException e) {
349                     LOGGER.error(e.getMessage());
350                 }
351             }
352 
353             readLines(lines, properties);
354 
355             try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(Paths.get(configFilePath)), StandardCharsets.ISO_8859_1))) {
356                 for (String value : lines) {
357                     writer.write(value);
358                     writer.newLine();
359                 }
360 
361                 // write remaining lines
362                 Enumeration<?> propertyEnumeration = properties.propertyNames();
363                 while (propertyEnumeration.hasMoreElements()) {
364                     String propertyName = (String) propertyEnumeration.nextElement();
365                     writer.write(propertyName + "=" + escapeValue(properties.getProperty(propertyName)));
366                     writer.newLine();
367                 }
368             } catch (IOException e) {
369                 DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_STORE_SETTINGS"), e);
370             }
371         }
372         updateLoggingConfig();
373     }
374 
375     private static void readLines(ArrayList<String> lines, Properties properties) {
376         try {
377             File configFile = new File(configFilePath);
378             if (configFile.exists()) {
379                 try (BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(configFile.toPath()), StandardCharsets.ISO_8859_1))) {
380                     String line;
381                     while ((line = reader.readLine()) != null) {
382                         lines.add(convertLine(line, properties));
383                     }
384                 }
385             }
386         } catch (IOException e) {
387             DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_LOAD_SETTINGS"), e);
388         }
389     }
390 
391     /**
392      * Convert input property line to new line with value from properties.
393      * Preserve comments
394      *
395      * @param line       input line
396      * @param properties new property values
397      * @return new line
398      */
399     private static String convertLine(String line, Properties properties) {
400         int hashIndex = line.indexOf('#');
401         int equalsIndex = line.indexOf('=');
402         // allow # in values, no a comment
403         // comments are pass through
404         if (equalsIndex >= 0 && (hashIndex < 0 || hashIndex >= equalsIndex)) {
405             String key = line.substring(0, equalsIndex);
406             String value = properties.getProperty(key);
407             if (value != null) {
408                 // build property with new value
409                 line = key + "=" + escapeValue(value);
410                 // remove property from source
411                 properties.remove(key);
412             }
413         }
414         return line;
415     }
416 
417     /**
418      * Escape backslash in value.
419      *
420      * @param value value
421      * @return escaped value
422      */
423     private static String escapeValue(String value) {
424         StringBuilder buffer = new StringBuilder();
425         for (char c : value.toCharArray()) {
426             if (c == '\\') {
427                 buffer.append('\\');
428             }
429             buffer.append(c);
430         }
431         return buffer.toString();
432     }
433 
434 
435     /**
436      * Get a property value as String.
437      *
438      * @param property property name
439      * @return property value
440      */
441     public static synchronized String getProperty(String property) {
442         String value = SETTINGS_PROPERTIES.getProperty(property);
443         // return null on empty value
444         if (value != null && value.isEmpty()) {
445             value = null;
446         }
447         return value;
448     }
449 
450     /**
451      * Get property value or default.
452      *
453      * @param property     property name
454      * @param defaultValue default property value
455      * @return property value
456      */
457     public static synchronized String getProperty(String property, String defaultValue) {
458         String value = getProperty(property);
459         if (value == null) {
460             value = defaultValue;
461         }
462         return value;
463     }
464 
465 
466     /**
467      * Get a property value as char[].
468      *
469      * @param property property name
470      * @return property value
471      */
472     public static synchronized char[] getCharArrayProperty(String property) {
473         String propertyValue = Settings.getProperty(property);
474         char[] value = null;
475         if (propertyValue != null) {
476             value = propertyValue.toCharArray();
477         }
478         return value;
479     }
480 
481     /**
482      * Set a property value.
483      *
484      * @param property property name
485      * @param value    property value
486      */
487     public static synchronized void setProperty(String property, String value) {
488         if (value != null) {
489             SETTINGS_PROPERTIES.setProperty(property, value);
490         } else {
491             SETTINGS_PROPERTIES.setProperty(property, "");
492         }
493     }
494 
495     /**
496      * Get a property value as int.
497      *
498      * @param property property name
499      * @return property value
500      */
501     public static synchronized int getIntProperty(String property) {
502         return getIntProperty(property, 0);
503     }
504 
505     /**
506      * Get a property value as int, return default value if null.
507      *
508      * @param property     property name
509      * @param defaultValue default property value
510      * @return property value
511      */
512     public static synchronized int getIntProperty(String property, int defaultValue) {
513         int value = defaultValue;
514         try {
515             String propertyValue = SETTINGS_PROPERTIES.getProperty(property);
516             if (propertyValue != null && !propertyValue.isEmpty()) {
517                 value = Integer.parseInt(propertyValue);
518             }
519         } catch (NumberFormatException e) {
520             DavGatewayTray.error(new BundleMessage("LOG_INVALID_SETTING_VALUE", property), e);
521         }
522         return value;
523     }
524 
525     /**
526      * Get a property value as boolean.
527      *
528      * @param property property name
529      * @return property value
530      */
531     public static synchronized boolean getBooleanProperty(String property) {
532         String propertyValue = SETTINGS_PROPERTIES.getProperty(property);
533         return Boolean.parseBoolean(propertyValue);
534     }
535 
536     /**
537      * Get a property value as boolean.
538      *
539      * @param property     property name
540      * @param defaultValue default property value
541      * @return property value
542      */
543     public static synchronized boolean getBooleanProperty(String property, boolean defaultValue) {
544         boolean value = defaultValue;
545         String propertyValue = SETTINGS_PROPERTIES.getProperty(property);
546         if (propertyValue != null && !propertyValue.isEmpty()) {
547             value = Boolean.parseBoolean(propertyValue);
548         }
549         return value;
550     }
551 
552     public static synchronized String loadRefreshToken(String username) {
553         String tokenFilePath = Settings.getProperty("davmail.oauth.tokenFilePath");
554         if (isEmpty(tokenFilePath)) {
555             return Settings.getProperty("davmail.oauth." + username.toLowerCase() + ".refreshToken");
556         } else {
557             return loadtokenFromFile(tokenFilePath, username.toLowerCase());
558         }
559     }
560 
561 
562     public static synchronized void storeRefreshToken(String username, String refreshToken) {
563         String tokenFilePath = Settings.getProperty("davmail.oauth.tokenFilePath");
564         if (isEmpty(tokenFilePath)) {
565             Settings.setProperty("davmail.oauth." + username.toLowerCase() + ".refreshToken", refreshToken);
566             Settings.save();
567         } else {
568             savetokentoFile(tokenFilePath, username.toLowerCase(), refreshToken);
569         }
570     }
571 
572     /**
573      * Persist token in davmail.oauth.tokenFilePath.
574      *
575      * @param tokenFilePath token file path
576      * @param username      username
577      * @param refreshToken  Oauth refresh token
578      */
579     private static void savetokentoFile(String tokenFilePath, String username, String refreshToken) {
580         try {
581             checkCreateTokenFilePath(tokenFilePath);
582             Properties properties = new Properties();
583             try (FileInputStream fis = new FileInputStream(tokenFilePath)) {
584                 properties.load(fis);
585             }
586             properties.setProperty(username, refreshToken);
587             try (FileOutputStream fos = new FileOutputStream(tokenFilePath)) {
588                 properties.store(fos, "Oauth tokens");
589             }
590         } catch (IOException e) {
591             Logger.getLogger(Settings.class).warn(e + " " + e.getMessage());
592         }
593     }
594 
595     /**
596      * Load token from davmail.oauth.tokenFilePath.
597      *
598      * @param tokenFilePath token file path
599      * @param username      username
600      * @return encrypted token value
601      */
602     private static String loadtokenFromFile(String tokenFilePath, String username) {
603         try {
604             checkCreateTokenFilePath(tokenFilePath);
605             Properties properties = new Properties();
606             try (FileInputStream fis = new FileInputStream(tokenFilePath)) {
607                 properties.load(fis);
608             }
609             return properties.getProperty(username);
610         } catch (IOException e) {
611             Logger.getLogger(Settings.class).warn(e + " " + e.getMessage());
612         }
613         return null;
614     }
615 
616     private static void checkCreateTokenFilePath(String tokenFilePath) throws IOException {
617         File file = new File(tokenFilePath);
618         File parentFile = file.getParentFile();
619         if (parentFile != null && (parentFile.mkdirs())) {
620                 LOGGER.info("Created token file directory "+parentFile.getAbsolutePath());
621 
622         }
623         if (file.createNewFile()) {
624             LOGGER.info("Created token file "+tokenFilePath);
625         }
626     }
627 
628     /**
629      * Build logging properties prefix.
630      *
631      * @param category logging category
632      * @return prefix
633      */
634     private static String getLoggingPrefix(String category) {
635         String prefix;
636         if ("rootLogger".equals(category)) {
637             prefix = "log4j.";
638         } else {
639             prefix = "log4j.logger.";
640         }
641         return prefix;
642     }
643 
644     /**
645      * Return Log4J logging level for the category.
646      *
647      * @param category logging category
648      * @return logging level
649      */
650     public static synchronized Level getLoggingLevel(String category) {
651         String prefix = getLoggingPrefix(category);
652         String currentValue = SETTINGS_PROPERTIES.getProperty(prefix + category);
653 
654         if (currentValue != null && !currentValue.isEmpty()) {
655             return Level.toLevel(currentValue);
656         } else if ("rootLogger".equals(category)) {
657             return Logger.getRootLogger().getLevel();
658         } else {
659             return Logger.getLogger(category).getLevel();
660         }
661     }
662 
663     /**
664      * Get all properties that are in the specified scope, that is, that start with '&lt;scope&gt;.'.
665      *
666      * @param scope start of property name
667      * @return properties
668      */
669     public static synchronized Properties getSubProperties(String scope) {
670         final String keyStart;
671         if (scope == null || scope.isEmpty()) {
672             keyStart = "";
673         } else if (scope.endsWith(".")) {
674             keyStart = scope;
675         } else {
676             keyStart = scope + '.';
677         }
678         Properties result = new Properties();
679         for (Map.Entry<Object, Object> entry : SETTINGS_PROPERTIES.entrySet()) {
680             String key = (String) entry.getKey();
681             if (key.startsWith(keyStart)) {
682                 String value = (String) entry.getValue();
683                 result.setProperty(key.substring(keyStart.length()), value);
684             }
685         }
686         return result;
687     }
688 
689     /**
690      * Set Log4J logging level for the category
691      *
692      * @param category logging category
693      * @param level    logging level
694      */
695     public static synchronized void setLoggingLevel(String category, Level level) {
696         if (level != null) {
697             String prefix = getLoggingPrefix(category);
698             SETTINGS_PROPERTIES.setProperty(prefix + category, level.toString());
699             if ("rootLogger".equals(category)) {
700                 Logger.getRootLogger().setLevel(level);
701             } else {
702                 Logger.getLogger(category).setLevel(level);
703             }
704         }
705     }
706 
707     /**
708      * Change and save a single property.
709      *
710      * @param property property name
711      * @param value    property value
712      */
713     public static synchronized void saveProperty(String property, String value) {
714         Settings.load();
715         Settings.setProperty(property, value);
716         Settings.save();
717     }
718 
719     /**
720      * Test if running on Windows
721      *
722      * @return true on Windows
723      */
724     public static boolean isWindows() {
725         return System.getProperty("os.name").toLowerCase().startsWith("windows");
726     }
727 
728     /**
729      * Test if running on Linux
730      *
731      * @return true on Linux
732      */
733     public static boolean isLinux() {
734         return System.getProperty("os.name").toLowerCase().startsWith("linux");
735     }
736 
737     public static boolean isUnix() {
738         return isLinux() ||
739                 System.getProperty("os.name").toLowerCase().startsWith("freebsd");
740     }
741 
742     public static String getUserAgent() {
743         return getProperty("davmail.userAgent", Settings.EDGE_USER_AGENT);
744     }
745 
746     public static String getOutlookUrl() {
747         String tld = getProperty("davmail.tld");
748         String outlookUrl = getProperty("davmail.outlookUrl");
749         if (outlookUrl != null) {
750             return outlookUrl;
751         } else if (tld == null) {
752             return OUTLOOK_URL;
753         } else {
754             return  "https://outlook.office365."+tld;
755         }
756     }
757 
758     public static String getO365Url() {
759         String tld = getProperty("davmail.tld");
760         String outlookUrl = getProperty("davmail.outlookUrl");
761         if (outlookUrl != null) {
762             return outlookUrl+"/EWS/Exchange.asmx";
763         } else if (tld == null) {
764             return O365_URL;
765         } else {
766             return  "https://outlook.office365."+tld+"/EWS/Exchange.asmx";
767         }
768     }
769 
770     public static String getO365LoginUrl() {
771         String tld = getProperty("davmail.tld");
772         String loginUrl = getProperty("davmail.loginUrl");
773         if (loginUrl != null) {
774             return loginUrl;
775         } else if (tld == null) {
776             return O365_LOGIN_URL;
777         } else {
778             return  "https://login.microsoftonline."+tld;
779         }
780     }
781 }