diff --git a/doc/admin.md b/doc/admin.md
index 95898c1e376ec095fcb8f2582dc48c51c7e5729c..56447c737f2c3268987f12543ddbd4a6628d03ed 100644
--- a/doc/admin.md
+++ b/doc/admin.md
@@ -7,7 +7,15 @@ Then open the Mail app from the app menu. Put in your mail account credentials a
 
 ## Configuration
 
-Certain advanced or experimental features need to be specifically enabled in your `config.php`:
+### Attachment size limit
+
+Admins can prevent users from attaching large attachments to their emails. Users will be asked to use link shares instead.
+
+```php
+'app.mail.attachment-size-limit' => 3*1024*1024,
+```
+
+The unit is bytes. The example about with limit to 3MB attachments. The default is 0 bytes which means no upload limit.
 
 ### Timeouts
 Depending on your mail host, it may be necessary to increase your IMAP and/or SMTP timeout threshold. Currently IMAP defaults to 20 seconds and SMTP defaults to 2 seconds. They can be changed as follows:
diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php
index 4577bc9833f3bf0a4ba6c12f5fd1ce54c6a9af10..9be84d4a830830350a3f0a97ee4b331bd580501e 100644
--- a/lib/Controller/PageController.php
+++ b/lib/Controller/PageController.php
@@ -134,6 +134,7 @@ class PageController extends Controller {
 		$response = new TemplateResponse($this->appName, 'index',
 			[
 				'debug' => $this->config->getSystemValue('debug', false),
+				'attachment-size-limit' => $this->config->getSystemValue('app.mail.attachment-size-limit', 0),
 				'app-version' => $this->config->getAppValue('mail', 'installed_version'),
 				'accounts' => base64_encode(json_encode($accountsJson)),
 				'external-avatars' => $this->preferences->getPreference('external-avatars', 'true'),
diff --git a/package-lock.json b/package-lock.json
index 673abb499d51bc4c8aa305a4fa078b7a472ef393..d2ddca70b089b7e4d2c98929166c57b7f06e90f9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3724,6 +3724,11 @@
         }
       }
     },
+    "base-64": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
+      "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs="
+    },
     "base64-js": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
@@ -5511,7 +5516,7 @@
     },
     "domelementtype": {
       "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
+      "resolved": "http://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
       "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="
     },
     "domexception": {
@@ -6651,6 +6656,11 @@
       "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
       "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
     },
+    "fast-xml-parser": {
+      "version": "3.17.5",
+      "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.17.5.tgz",
+      "integrity": "sha512-lEvThd1Xq+CCylf1n+05bUZCDZjTufaaaqpxM3JZ+4iDqtlG+d/oKgtMmg9GEMOuzBgUoalIzFOaClht9YiGJQ=="
+    },
     "fastest-levenshtein": {
       "version": "1.0.12",
       "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz",
@@ -7275,6 +7285,11 @@
       "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
       "dev": true
     },
+    "hot-patcher": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-0.5.0.tgz",
+      "integrity": "sha512-2Uu2W0s8+dnqXzdlg0MRsRzPoDCs1wVjOGSyMRRaMzLDX4bgHw6xDYKccsWafXPPxQpkQfEjgW6+17pwcg60bw=="
+    },
     "hsl-regex": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz",
@@ -9323,6 +9338,11 @@
       "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
       "dev": true
     },
+    "nested-property": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/nested-property/-/nested-property-4.0.0.tgz",
+      "integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA=="
+    },
     "nextcloud_issuetemplate_builder": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/nextcloud_issuetemplate_builder/-/nextcloud_issuetemplate_builder-0.1.0.tgz",
@@ -10111,6 +10131,11 @@
       "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
       "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
     },
+    "path-posix": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz",
+      "integrity": "sha1-BrJhE/Vr6rBCVFojv6iAA8ysJg8="
+    },
     "path-to-regexp": {
       "version": "1.8.0",
       "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
@@ -11263,6 +11288,11 @@
       "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
       "dev": true
     },
+    "querystringify": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+      "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
+    },
     "quick-lru": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz",
@@ -11811,6 +11841,11 @@
       "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
       "dev": true
     },
+    "requires-port": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+      "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
+    },
     "resolve": {
       "version": "1.17.0",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
@@ -13741,6 +13776,11 @@
         }
       }
     },
+    "url-join": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
+      "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
+    },
     "url-loader": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz",
@@ -13794,6 +13834,15 @@
         }
       }
     },
+    "url-parse": {
+      "version": "1.4.7",
+      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz",
+      "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==",
+      "requires": {
+        "querystringify": "^2.1.1",
+        "requires-port": "^1.0.0"
+      }
+    },
     "use": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
@@ -14275,6 +14324,38 @@
         "chokidar": "^2.1.8"
       }
     },
+    "webdav": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/webdav/-/webdav-3.6.1.tgz",
+      "integrity": "sha512-WRE6M4iePpd4TPJ0l5PCWMuoZS4UOHawtf/fr7FcmTZWsSjx1syywyW3TbgEFHptqyh4mMoLUrv8dAUtmTqbEg==",
+      "requires": {
+        "axios": "^0.20.0",
+        "base-64": "^0.1.0",
+        "fast-xml-parser": "^3.17.4",
+        "he": "^1.2.0",
+        "hot-patcher": "^0.5.0",
+        "minimatch": "^3.0.4",
+        "nested-property": "^4.0.0",
+        "path-posix": "^1.0.0",
+        "url-join": "^4.0.1",
+        "url-parse": "^1.4.7"
+      },
+      "dependencies": {
+        "axios": {
+          "version": "0.20.0",
+          "resolved": "https://registry.npmjs.org/axios/-/axios-0.20.0.tgz",
+          "integrity": "sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA==",
+          "requires": {
+            "follow-redirects": "^1.10.0"
+          }
+        },
+        "follow-redirects": {
+          "version": "1.13.0",
+          "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz",
+          "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA=="
+        }
+      }
+    },
     "webidl-conversions": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
diff --git a/package.json b/package.json
index 09fa8db8ec137cc0f8b9f75f23c4f11a923a8f06..2e180949af99f511eeabe5df1357bb91021f9ed6 100644
--- a/package.json
+++ b/package.json
@@ -73,7 +73,8 @@
     "vue-slide-up-down": "^2.0.1",
     "vue-tabs-component": "^1.5.0",
     "vuex": "^3.6.0",
-    "vuex-router-sync": "^5.0.0"
+    "vuex-router-sync": "^5.0.0",
+    "webdav": "^3.6.1"
   },
   "browserslist": [
     "extends @nextcloud/browserslist-config"
diff --git a/src/components/Composer.vue b/src/components/Composer.vue
index 8258f131d3749c0f87473dce89a72fe9a95357d0..76b95746d2c8bed6b3f0f098fbb0fa4ce3eada22 100644
--- a/src/components/Composer.vue
+++ b/src/components/Composer.vue
@@ -143,7 +143,10 @@
 				:is-reply-or-forward="isReply || isForward" />
 		</div>
 		<div class="composer-actions">
-			<ComposerAttachments v-model="attachments" :bus="bus" @upload="onAttachmentsUploading" />
+			<ComposerAttachments v-model="attachments"
+				:bus="bus"
+				:upload-size-limit="attachmentSizeLimit"
+				@upload="onAttachmentsUploading" />
 			<div class="composer-actions-right">
 				<p class="composer-actions-draft">
 					<span v-if="!canSaveDraft" id="draft-status">{{ t('mail', 'Can not save draft because this account does not have a drafts mailbox configured.') }}</span>
@@ -385,6 +388,9 @@ export default {
 		allRecipients() {
 			return this.selectTo.concat(this.selectCc).concat(this.selectBcc)
 		},
+		attachmentSizeLimit() {
+			return this.$store.getters.getPreference('attachment-size-limit')
+		},
 		selectableRecipients() {
 			return this.newRecipients
 				.concat(this.autocompleteRecipients)
diff --git a/src/components/ComposerAttachments.vue b/src/components/ComposerAttachments.vue
index cb0a3d3427da555057437859edcd08f8183858f6..fb829af768338cf1c595d741fb95e01e59cd8b12 100644
--- a/src/components/ComposerAttachments.vue
+++ b/src/components/ComposerAttachments.vue
@@ -47,13 +47,19 @@
 import map from 'lodash/fp/map'
 import trimStart from 'lodash/fp/trimCharsStart'
 import { getRequestToken } from '@nextcloud/auth'
-import { translate as t } from '@nextcloud/l10n'
-import { getFilePickerBuilder } from '@nextcloud/dialogs'
+import { formatFileSize } from '@nextcloud/files'
+import prop from 'lodash/fp/prop'
+import { getFilePickerBuilder, showWarning } from '@nextcloud/dialogs'
+import sum from 'lodash/fp/sum'
+import sumBy from 'lodash/fp/sumBy'
+import { translate as t, translatePlural as n } from '@nextcloud/l10n'
+
 import Vue from 'vue'
 
-import Logger from '../logger'
-import { uploadLocalAttachment } from '../service/AttachmentService'
+import logger from '../logger'
+import { getFileSize } from '../service/FileService'
 import { shareFile } from '../service/FileSharingService'
+import { uploadLocalAttachment } from '../service/AttachmentService'
 
 export default {
 	name: 'ComposerAttachments',
@@ -66,6 +72,10 @@ export default {
 			type: Object,
 			required: true,
 		},
+		uploadSizeLimit: {
+			type: Number,
+			default: 0,
+		},
 	},
 	data() {
 		return {
@@ -93,22 +103,48 @@ export default {
 		onAddLocalAttachment() {
 			this.$refs.localAttachments.click()
 		},
-		fileNameToAttachment(name, id) {
+		fileNameToAttachment(name, size, id) {
 			return {
 				fileName: name,
 				displayName: trimStart('/')(name),
 				id,
+				size,
 				isLocal: id !== undefined,
 			}
 		},
 		emitNewAttachments(attachments) {
 			this.$emit('input', this.value.concat(attachments))
 		},
+		totalSizeOfUpload() {
+			return Object.values(this.value).reduce((acc, upload) => {
+				if (!upload.isLocal) {
+					// Ignore link shares
+					return acc
+				}
+
+				return acc + upload.size
+			}, 0)
+		},
 		onLocalAttachmentSelected(e) {
 			this.uploading = true
 
 			Vue.set(this, 'uploads', {})
 
+			const toUpload = sumBy(prop('size'), Object.values(e.target.files))
+			const newTotal = toUpload + this.totalSizeOfUpload()
+			logger.debug('checking upload size limit', {
+				existingUploads: this.totalSizeOfUpload(),
+				toUpload,
+				limit: this.uploadSizeLimit,
+				newTotal,
+			})
+			if (this.uploadSizeLimit && newTotal > this.uploadSizeLimit) {
+				this.showAttachmentFileSizeWarning(e.target.files.length)
+
+				this.uploading = false
+				return
+			}
+
 			const progress = (id) => (prog, uploaded) => {
 				this.uploads[id].uploaded = uploaded
 			}
@@ -120,38 +156,60 @@ export default {
 				})
 
 				return uploadLocalAttachment(file, progress(file.name)).then(({ file, id }) => {
-					Logger.info('uploaded')
-					return this.emitNewAttachments([this.fileNameToAttachment(file.name, id)])
+					logger.info('uploaded')
+					return this.emitNewAttachments([this.fileNameToAttachment(file.name, file.size, id)])
 				})
-			})(e.target.files)
+			}, e.target.files)
 
 			const done = Promise.all(promises)
-				.catch((error) => Logger.error('could not upload all attachments', { error }))
+				.catch((error) => logger.error('could not upload all attachments', { error }))
 				.then(() => (this.uploading = false))
 
 			this.$emit('upload', done)
 
 			return done
 		},
-		onAddCloudAttachment() {
+		async onAddCloudAttachment() {
 			const picker = getFilePickerBuilder(t('mail', 'Choose a file to add as attachment')).setMultiSelect(true).build()
 
-			return picker
-				.pick(t('mail', 'Choose a file to add as attachment'))
-				.then((paths) => this.emitNewAttachments(paths.map(this.fileNameToAttachment)))
-				.catch((error) => Logger.error('could not choose a file as attachment', { error }))
+			try {
+				const paths = await picker.pick(t('mail', 'Choose a file to add as attachment'))
+				const fileSizes = await Promise.all(paths.map(getFileSize))
+				const newTotal = sum(fileSizes) + this.totalSizeOfUpload()
+
+				if (this.uploadSizeLimit && newTotal > this.uploadSizeLimit) {
+					this.showAttachmentFileSizeWarning(paths.length)
+
+					return
+				}
+
+				this.emitNewAttachments(paths.map(this.fileNameToAttachment))
+			} catch (error) {
+				logger.error('could not choose a file as attachment', { error })
+			}
 		},
-		onAddCloudAttachmentLink() {
+		async onAddCloudAttachmentLink() {
 			const picker = getFilePickerBuilder(t('mail', 'Choose a file to share as a link')).build()
 
-			return picker
-				.pick(t('mail', 'Choose a file to share as a link'))
-				.then(async(path) => {
-					const url = await shareFile(path, getRequestToken())
+			try {
+				const path = await picker.pick(t('mail', 'Choose a file to share as a link'))
+				const url = await shareFile(path, getRequestToken())
 
-					return this.appendToBodyAtCursor(`<a href="${url}">${url}</a>`)
-				})
-				.catch((error) => Logger.error('could not choose a file as attachment link', { error }))
+				this.appendToBodyAtCursor(`<a href="${url}">${url}</a>`)
+			} catch (error) {
+				logger.error('could not choose a file as attachment link', { error })
+			}
+		},
+		showAttachmentFileSizeWarning(num) {
+			showWarning(n(
+				'mail',
+				'The attachment exceed the allowed attachments size of {size}. Please share the file via link instead.',
+				'The attachments exceed the allowed attachments size of {size}. Please share the files via link instead.',
+				num,
+				{
+					size: formatFileSize(this.uploadSizeLimit),
+				}
+			))
 		},
 		onDelete(attachment) {
 			this.$emit(
diff --git a/src/main.js b/src/main.js
index 1b26ac3228e97cb4ddb9ccfaf3258f2fb04e4eda..1bf4d7676aeabe56d683db6268491f0c93636c8c 100644
--- a/src/main.js
+++ b/src/main.js
@@ -60,6 +60,10 @@ store.commit('savePreference', {
 	key: 'debug',
 	value: getPreferenceFromPage('debug-mode'),
 })
+store.commit('savePreference', {
+	key: 'attachment-size-limit',
+	value: Number.parseInt(getPreferenceFromPage('attachment-size-limit'), 10),
+})
 store.commit('savePreference', {
 	key: 'version',
 	value: getPreferenceFromPage('config-installed-version'),
diff --git a/src/service/FileService.js b/src/service/FileService.js
new file mode 100644
index 0000000000000000000000000000000000000000..d37da3a8f674e7cd3673127a44ee043d32b8c937
--- /dev/null
+++ b/src/service/FileService.js
@@ -0,0 +1,54 @@
+/*
+ * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import axios from '@nextcloud/axios'
+import { generateRemoteUrl } from '@nextcloud/router'
+import { getCurrentUser } from '@nextcloud/auth'
+import memoize from 'lodash/fp/memoize'
+import webdav from 'webdav'
+
+const getWebDavClient = memoize(() => {
+	// Add this so the server knows it is an request from the browser
+	axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
+
+	// force our axios
+	const patcher = webdav.getPatcher()
+	patcher.patch('request', axios)
+
+	return webdav.createClient(
+		generateRemoteUrl(`dav/files/${getCurrentUser().uid}`)
+	)
+})
+
+export async function getFileSize(path) {
+	const response = await getWebDavClient().stat(path, {
+		data: `<?xml version="1.0"?>
+			<d:propfind  xmlns:d="DAV:"
+				xmlns:oc="http://owncloud.org/ns">
+				<d:prop>
+					<oc:size />
+				</d:prop>
+			</d:propfind>`,
+		details: true,
+	})
+
+	return response?.data?.props?.size
+}
diff --git a/templates/index.php b/templates/index.php
index dbee99b902ab36ae40617f781a8afb5f35237a8d..5c804b86c07ac39b1375f374986f1e093184a79f 100644
--- a/templates/index.php
+++ b/templates/index.php
@@ -29,6 +29,7 @@ script('mail', 'mail');
 ?>
 
 <input type="hidden" id="debug-mode" value="<?php p($_['debug'] ? 'true' : 'false'); ?>">
+<input type="hidden" id="attachment-size-limit" value="<?php p($_['attachment-size-limit']); ?>">
 <input type="hidden" id="config-installed-version" value="<?php p($_['app-version']); ?>">
 <input type="hidden" id="serialized-accounts" value="<?php p($_['accounts']); ?>">
 <input type="hidden" id="external-avatars" value="<?php p($_['external-avatars']); ?>">
diff --git a/tests/Unit/Controller/PageControllerTest.php b/tests/Unit/Controller/PageControllerTest.php
index 9bbb9b7020f1494e29d57e4090f0a92a882c3ced..b7d9e2a927625b1edc855302e5200fb0ad91f673 100644
--- a/tests/Unit/Controller/PageControllerTest.php
+++ b/tests/Unit/Controller/PageControllerTest.php
@@ -196,10 +196,12 @@ class PageControllerTest extends TestCase {
 		$this->userSession->expects($this->once())
 			->method('getUser')
 			->will($this->returnValue($user));
-		$this->config->expects($this->once())
+		$this->config
 			->method('getSystemValue')
-			->with('debug', false)
-			->will($this->returnValue(true));
+			->willReturnMap([
+				['debug', false, true],
+				['app.mail.attachment-size-limit', 0, 123],
+			]);
 		$this->config->expects($this->once())
 			->method('getAppValue')
 			->with('mail', 'installed_version')
@@ -225,6 +227,7 @@ class PageControllerTest extends TestCase {
 		$expected = new TemplateResponse($this->appName, 'index',
 			[
 				'debug' => true,
+				'attachment-size-limit' => 123,
 				'external-avatars' => 'true',
 				'app-version' => '1.2.3',
 				'accounts' => base64_encode(json_encode($accountsJson)),