├── analysis_options.yaml
├── example
├── flutter
│ ├── ios
│ │ ├── Runner
│ │ │ ├── Runner-Bridging-Header.h
│ │ │ ├── Assets.xcassets
│ │ │ │ ├── LaunchImage.imageset
│ │ │ │ │ ├── LaunchImage.png
│ │ │ │ │ ├── LaunchImage@2x.png
│ │ │ │ │ ├── LaunchImage@3x.png
│ │ │ │ │ ├── README.md
│ │ │ │ │ └── Contents.json
│ │ │ │ └── AppIcon.appiconset
│ │ │ │ │ ├── Icon-App-20x20@1x.png
│ │ │ │ │ ├── Icon-App-20x20@2x.png
│ │ │ │ │ ├── Icon-App-20x20@3x.png
│ │ │ │ │ ├── Icon-App-29x29@1x.png
│ │ │ │ │ ├── Icon-App-29x29@2x.png
│ │ │ │ │ ├── Icon-App-29x29@3x.png
│ │ │ │ │ ├── Icon-App-40x40@1x.png
│ │ │ │ │ ├── Icon-App-40x40@2x.png
│ │ │ │ │ ├── Icon-App-40x40@3x.png
│ │ │ │ │ ├── Icon-App-60x60@2x.png
│ │ │ │ │ ├── Icon-App-60x60@3x.png
│ │ │ │ │ ├── Icon-App-76x76@1x.png
│ │ │ │ │ ├── Icon-App-76x76@2x.png
│ │ │ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ │ │ ├── Icon-App-83.5x83.5@2x.png
│ │ │ │ │ └── Contents.json
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Base.lproj
│ │ │ │ ├── Main.storyboard
│ │ │ │ └── LaunchScreen.storyboard
│ │ │ └── Info.plist
│ │ ├── Flutter
│ │ │ ├── Debug.xcconfig
│ │ │ ├── Release.xcconfig
│ │ │ └── AppFrameworkInfo.plist
│ │ ├── Runner.xcodeproj
│ │ │ ├── project.xcworkspace
│ │ │ │ └── contents.xcworkspacedata
│ │ │ ├── xcshareddata
│ │ │ │ └── xcschemes
│ │ │ │ │ └── Runner.xcscheme
│ │ │ └── project.pbxproj
│ │ ├── Runner.xcworkspace
│ │ │ └── contents.xcworkspacedata
│ │ ├── .gitignore
│ │ └── Podfile
│ ├── README.md
│ ├── android
│ │ ├── gradle.properties
│ │ ├── .gitignore
│ │ ├── app
│ │ │ ├── src
│ │ │ │ ├── main
│ │ │ │ │ ├── res
│ │ │ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ │ ├── values
│ │ │ │ │ │ │ └── styles.xml
│ │ │ │ │ │ └── drawable
│ │ │ │ │ │ │ └── launch_background.xml
│ │ │ │ │ ├── kotlin
│ │ │ │ │ │ └── com
│ │ │ │ │ │ │ └── example
│ │ │ │ │ │ │ └── mailto_example
│ │ │ │ │ │ │ └── MainActivity.kt
│ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ ├── debug
│ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ └── profile
│ │ │ │ │ └── AndroidManifest.xml
│ │ │ └── build.gradle
│ │ ├── gradle
│ │ │ └── wrapper
│ │ │ │ └── gradle-wrapper.properties
│ │ ├── settings.gradle
│ │ └── build.gradle
│ ├── .metadata
│ ├── pubspec.yaml
│ ├── .gitignore
│ └── lib
│ │ └── main.dart
├── README.md
└── http_server
│ └── mailto_example.dart
├── assets
├── dart-server.png
└── macos-client.png
├── coverage.sh
├── .travis.yml
├── CONTRIBUTE.md
├── pubspec.yaml
├── .gitignore
├── derry.yaml
├── CHANGELOG.md
├── LICENSE
├── lib
└── mailto.dart
├── README.md
└── test
└── mailto_test.dart
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:pedantic/analysis_options.yaml
2 |
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
--------------------------------------------------------------------------------
/assets/dart-server.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/assets/dart-server.png
--------------------------------------------------------------------------------
/assets/macos-client.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/assets/macos-client.png
--------------------------------------------------------------------------------
/example/flutter/README.md:
--------------------------------------------------------------------------------
1 | # mailto_example
2 |
3 | This is a Flutter example app of the [`mailto` package](https://pub.dev/packages/mailto).
4 |
--------------------------------------------------------------------------------
/example/flutter/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/flutter/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.enableR8=true
3 | android.useAndroidX=true
4 | android.enableJetifier=true
5 |
--------------------------------------------------------------------------------
/example/flutter/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # `mailto` examples
2 |
3 | You can read the examples' source code on [GitHub](https://github.com/smaho-engineering/mailto/tree/master/example).
4 |
--------------------------------------------------------------------------------
/coverage.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | pub global activate test_coverage
4 |
5 | pub global run test_coverage
6 |
7 | bash <(curl -s https://codecov.io/bash)
8 |
9 |
--------------------------------------------------------------------------------
/example/flutter/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
--------------------------------------------------------------------------------
/example/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: dart
2 |
3 | dart_task:
4 | - test
5 | - dartfmt
6 | - dartanalyzer: --fatal-warnings lib test
7 |
8 | jobs:
9 | include:
10 | - stage: coverage
11 | script: bash coverage.sh
12 |
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smaho-engineering/mailto/HEAD/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/example/flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/flutter/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jun 23 08:50:38 CEST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
7 |
--------------------------------------------------------------------------------
/example/flutter/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/CONTRIBUTE.md:
--------------------------------------------------------------------------------
1 | ## Development
2 |
3 | * **`dartfmt -w .`** - Format code with [`dartfmt`](https://dart.dev/tools/dartfmt)
4 | * **`pub run test`** - Run all tests using the [`test`](https://pub.dev/packages/test) package
5 | * **`dartanalyzer lib test`** - Run the analyzer on the `lib` and `test` folders
6 | * **`bash coverage.sh`** - Get coverage info
7 |
--------------------------------------------------------------------------------
/example/flutter/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 18cd7a3601bcffb36fdf2f679f763b5e827c2e8e
8 | channel: beta
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/example/flutter/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/flutter/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
--------------------------------------------------------------------------------
/example/flutter/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: mailto
2 | version: 2.0.0
3 | description: Simple Dart package for creating mailto links in your Flutter apps
4 | homepage: https://github.com/smaho-engineering/mailto
5 | repository: https://github.com/smaho-engineering/mailto
6 | issue_tracker: https://github.com/smaho-engineering/mailto/issues
7 | environment:
8 | sdk: '>=2.12.0 <3.0.0'
9 | dev_dependencies:
10 | pedantic: ^1.10.0-nullsafety.3
11 | test: ^1.16.0-nullsafety.13
12 | scripts: derry.yaml
13 |
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 |
4 | @UIApplicationMain
5 | @objc class AppDelegate: FlutterAppDelegate {
6 | override func application(
7 | _ application: UIApplication,
8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
9 | ) -> Bool {
10 | GeneratedPluginRegistrant.register(with: self)
11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/example/flutter/android/app/src/main/kotlin/com/example/mailto_example/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.mailto_example
2 |
3 | import androidx.annotation.NonNull;
4 | import io.flutter.embedding.android.FlutterActivity
5 | import io.flutter.embedding.engine.FlutterEngine
6 | import io.flutter.plugins.GeneratedPluginRegistrant
7 |
8 | class MainActivity: FlutterActivity() {
9 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
10 | GeneratedPluginRegistrant.registerWith(flutterEngine);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/example/flutter/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/example/flutter/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
4 |
5 | def plugins = new Properties()
6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
7 | if (pluginsFile.exists()) {
8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
9 | }
10 |
11 | plugins.each { name, path ->
12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
13 | include ":$name"
14 | project(":$name").projectDir = pluginDirectory
15 | }
16 |
--------------------------------------------------------------------------------
/example/flutter/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: mailto_example
2 | description: Demonstrates how to use the mailto package. Fancy iOS app.
3 | version: 1.0.0+1
4 | publish_to: none
5 | environment:
6 | sdk: ">=2.12.0 <3.0.0"
7 | dependencies:
8 | flutter:
9 | sdk: flutter
10 | mailto:
11 | path: ../../
12 | # Most likely, you will want to use the url_launcher in your Flutter app
13 | # if you are using the mailto package.
14 | url_launcher: ^6.0.3
15 | # For a better demo app, not necessary at all
16 | cupertino_icons: ^0.1.2
17 | dev_dependencies:
18 | flutter_test:
19 | sdk: flutter
20 |
--------------------------------------------------------------------------------
/example/flutter/ios/.gitignore:
--------------------------------------------------------------------------------
1 | *.mode1v3
2 | *.mode2v3
3 | *.moved-aside
4 | *.pbxuser
5 | *.perspectivev3
6 | **/*sync/
7 | .sconsign.dblite
8 | .tags*
9 | **/.vagrant/
10 | **/DerivedData/
11 | Icon?
12 | **/Pods/
13 | **/.symlinks/
14 | profile
15 | xcuserdata
16 | **/.generated/
17 | Flutter/App.framework
18 | Flutter/Flutter.framework
19 | Flutter/Flutter.podspec
20 | Flutter/Generated.xcconfig
21 | Flutter/app.flx
22 | Flutter/app.zip
23 | Flutter/flutter_assets/
24 | Flutter/flutter_export_environment.sh
25 | ServiceDefinitions.json
26 | Runner/GeneratedPluginRegistrant.*
27 |
28 | # Exceptions to above rules.
29 | !default.mode1v3
30 | !default.mode2v3
31 | !default.pbxuser
32 | !default.perspectivev3
33 |
--------------------------------------------------------------------------------
/example/flutter/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.3.50'
3 | repositories {
4 | google()
5 | jcenter()
6 | }
7 |
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:3.5.0'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | }
12 | }
13 |
14 | allprojects {
15 | repositories {
16 | google()
17 | jcenter()
18 | }
19 | }
20 |
21 | rootProject.buildDir = '../build'
22 | subprojects {
23 | project.buildDir = "${rootProject.buildDir}/${project.name}"
24 | }
25 | subprojects {
26 | project.evaluationDependsOn(':app')
27 | }
28 |
29 | task clean(type: Delete) {
30 | delete rootProject.buildDir
31 | }
32 |
--------------------------------------------------------------------------------
/example/flutter/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 |
12 | # IntelliJ related
13 | *.iml
14 | *.ipr
15 | *.iws
16 | .idea/
17 |
18 | # The .vscode folder contains launch configuration and tasks you configure in
19 | # VS Code which you may wish to be included in version control, so this line
20 | # is commented out by default.
21 | #.vscode/
22 |
23 | # Flutter/Dart/Pub related
24 | **/doc/api/
25 | .dart_tool/
26 | .flutter-plugins
27 | .flutter-plugins-dependencies
28 | .packages
29 | .pub-cache/
30 | .pub/
31 | /build/
32 |
33 | # Web related
34 | lib/generated_plugin_registrant.dart
35 |
36 | # Exceptions to above rules.
37 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
38 |
--------------------------------------------------------------------------------
/example/flutter/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 8.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io/api/dart
2 | # Edit at https://www.gitignore.io/?templates=dart
3 |
4 | ### Dart ###
5 | # See https://www.dartlang.org/guides/libraries/private-files
6 |
7 | # Coverage
8 | coverage/
9 | coverage_badge.svg
10 | test/.test_coverage.dart
11 |
12 |
13 | # Files and directories created by pub
14 | .dart_tool/
15 | .packages
16 | build/
17 |
18 | # If you're building an application, you may want to check-in your pubspec.lock
19 | pubspec.lock
20 |
21 | # JetBrains
22 | .idea/
23 |
24 | # Directory created by dartdoc
25 | # If you don't generate documentation locally you can remove this line.
26 | doc/api/
27 |
28 | # Avoid committing generated Javascript files:
29 | *.dart.js
30 | *.info.json # Produced by the --dump-info flag.
31 | *.js # When generated by dart2js. Don't specify *.js if your
32 | # project includes source files written in JavaScript.
33 | *.js_
34 | *.js.deps
35 | *.js.map
36 |
37 | # End of https://www.gitignore.io/api/dart
38 |
--------------------------------------------------------------------------------
/derry.yaml:
--------------------------------------------------------------------------------
1 | get: pub get
2 | format: dartfmt -w --set-exit-if-changed .
3 | analyze: dart analyze --fatal-infos --fatal-warnings .
4 | test: dart test
5 | test-coverage:
6 | - pub global activate test_coverage
7 | - pub global run test_coverage
8 | - genhtml coverage/lcov.info -o coverage/html
9 | example-http-server:
10 | - cd example/http_server && dartfmt -w --set-exit-if-changed .
11 | - cd example/http_server && dart analyze --fatal-infos --fatal-warnings .
12 | - cd example/http_server && dart compile aot-snapshot whatsapp_unilink_example.dart
13 | example-flutter:
14 | - cd example/flutter && flutter format --set-exit-if-changed .
15 | - cd example/flutter && flutter analyze --fatal-infos --fatal-warnings .
16 | - cd example/flutter && flutter build apk
17 | publish-dry-run:
18 | - pub publish --dry-run
19 | # Verify everything
20 | check:
21 | - $get
22 | - $format
23 | - $analyze
24 | - $test
25 | - $example-http-server
26 | - $example-flutter
27 | - $test-coverage
28 | - $publish-dry-run
29 |
--------------------------------------------------------------------------------
/example/http_server/mailto_example.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:mailto/mailto.dart';
4 |
5 | String renderHtml(Mailto mailto) => '''
6 |
7 |
8 | mailto example
9 |
10 |
11 | Open mail client
12 |
13 |
14 | ''';
15 |
16 | Future main() async {
17 | final mailto = Mailto(
18 | to: [
19 | 'example@example.com',
20 | 'ejemplo@ejemplo.com',
21 | ],
22 | cc: [
23 | 'percentage%100@example.com',
24 | 'QuestionMark?address@example.com',
25 | ],
26 | bcc: [
27 | 'Mike&family@example.org',
28 | ],
29 | subject: 'Let\'s drink a "café"! ☕️ 2+2=4 #coffeeAndMath',
30 | body:
31 | 'Hello this if the first line!\n\nNew line with some special characters őúóüűáéèßáñ\nEmoji: 🤪💙👍',
32 | );
33 |
34 | final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 3000);
35 |
36 | print('Listening on http://localhost:${server.port}');
37 |
38 | await for (HttpRequest request in server) {
39 | print('received request!');
40 | request.response
41 | ..statusCode = HttpStatus.ok
42 | ..headers.contentType = ContentType.html
43 | ..write(renderHtml(mailto));
44 | await request.response.close();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 2.0.0
2 |
3 | * Migrate package to Dart's null safety language feature, requiring Dart 2.12 or higher. Close [`mailto #7`](https://github.com/smaho-engineering/mailto/issues/7)
4 | * Make `to` values not required, do not depend on the `meta` package anymore. Fix [`mailto #4`](https://github.com/smaho-engineering/mailto/issues/4)
5 |
6 | ## 1.1.0
7 |
8 | * `to` values were made optional [`mailto #4`](https://github.com/smaho-engineering/mailto/issues/4)
9 | * removed the `validate` argument from `Mailto`. You may still validate paramaters using the `Mailto.validateParameters` static function.
10 |
11 | ## 1.0.0
12 |
13 | * Correctly encode trickier "to" arguments from the RFC linked in the README
14 | * Validate input in constructor and throw `ArgumentError` if something is incorrect
15 | * Make validation optional by setting the `validate` argument to `MailtoValidate.never
16 | * Clarify in README the limitations of mailto links
17 | * Add server example so it's easy to test on desktop/browser
18 | * Add screenshots about example Flutter app and server
19 | * README improvements
20 | * Fix pub.dev pub points
21 |
22 | ## 0.1.4
23 |
24 | Improve README
25 |
26 | ## 0.1.3
27 |
28 | Write README and Flutter example app
29 |
30 | ## 0.1.2
31 |
32 | Add the `meta` package as dependency
33 |
34 | ## 0.1.1
35 |
36 | Add changelog and example command-line script
37 |
38 | ## 0.1.0
39 |
40 | Initial version of the `mailto` package
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2019, SMAHO GmbH.
2 |
3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4 |
5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6 |
7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8 |
9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10 |
11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12 |
--------------------------------------------------------------------------------
/example/flutter/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
8 |
12 |
19 |
20 |
21 |
22 |
23 |
24 |
26 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | mailto_example
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | $(FLUTTER_BUILD_NAME)
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(FLUTTER_BUILD_NUMBER)
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UISupportedInterfaceOrientations
30 |
31 | UIInterfaceOrientationPortrait
32 | UIInterfaceOrientationLandscapeLeft
33 | UIInterfaceOrientationLandscapeRight
34 |
35 | UISupportedInterfaceOrientations~ipad
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationPortraitUpsideDown
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UIViewControllerBasedStatusBarAppearance
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/example/flutter/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | def localProperties = new Properties()
2 | def localPropertiesFile = rootProject.file('local.properties')
3 | if (localPropertiesFile.exists()) {
4 | localPropertiesFile.withReader('UTF-8') { reader ->
5 | localProperties.load(reader)
6 | }
7 | }
8 |
9 | def flutterRoot = localProperties.getProperty('flutter.sdk')
10 | if (flutterRoot == null) {
11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12 | }
13 |
14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
15 | if (flutterVersionCode == null) {
16 | flutterVersionCode = '1'
17 | }
18 |
19 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
20 | if (flutterVersionName == null) {
21 | flutterVersionName = '1.0'
22 | }
23 |
24 | apply plugin: 'com.android.application'
25 | apply plugin: 'kotlin-android'
26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
27 |
28 | android {
29 | compileSdkVersion 28
30 |
31 | sourceSets {
32 | main.java.srcDirs += 'src/main/kotlin'
33 | }
34 |
35 | lintOptions {
36 | disable 'InvalidPackage'
37 | }
38 |
39 | defaultConfig {
40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
41 | applicationId "com.example.mailto_example"
42 | minSdkVersion 16
43 | targetSdkVersion 28
44 | versionCode flutterVersionCode.toInteger()
45 | versionName flutterVersionName
46 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
47 | }
48 |
49 | buildTypes {
50 | release {
51 | // TODO: Add your own signing config for the release build.
52 | // Signing with the debug keys for now, so `flutter run --release` works.
53 | signingConfig signingConfigs.debug
54 | }
55 | }
56 | }
57 |
58 | flutter {
59 | source '../..'
60 | }
61 |
62 | dependencies {
63 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
64 | testImplementation 'junit:junit:4.12'
65 | androidTestImplementation 'androidx.test:runner:1.1.1'
66 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
67 | }
68 |
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon-App-1024x1024@1x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/example/flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/example/flutter/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | project 'Runner', {
8 | 'Debug' => :debug,
9 | 'Profile' => :release,
10 | 'Release' => :release,
11 | }
12 |
13 | def parse_KV_file(file, separator='=')
14 | file_abs_path = File.expand_path(file)
15 | if !File.exists? file_abs_path
16 | return [];
17 | end
18 | generated_key_values = {}
19 | skip_line_start_symbols = ["#", "/"]
20 | File.foreach(file_abs_path) do |line|
21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
22 | plugin = line.split(pattern=separator)
23 | if plugin.length == 2
24 | podname = plugin[0].strip()
25 | path = plugin[1].strip()
26 | podpath = File.expand_path("#{path}", file_abs_path)
27 | generated_key_values[podname] = podpath
28 | else
29 | puts "Invalid plugin specification: #{line}"
30 | end
31 | end
32 | generated_key_values
33 | end
34 |
35 | target 'Runner' do
36 | use_frameworks!
37 | use_modular_headers!
38 |
39 | # Flutter Pod
40 |
41 | copied_flutter_dir = File.join(__dir__, 'Flutter')
42 | copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
43 | copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
44 | unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
45 | # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
46 | # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
47 | # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
48 |
49 | generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
50 | unless File.exist?(generated_xcode_build_settings_path)
51 | raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
52 | end
53 | generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
54 | cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];
55 |
56 | unless File.exist?(copied_framework_path)
57 | FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
58 | end
59 | unless File.exist?(copied_podspec_path)
60 | FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
61 | end
62 | end
63 |
64 | # Keep pod path relative so it can be checked into Podfile.lock.
65 | pod 'Flutter', :path => 'Flutter'
66 |
67 | # Plugin Pods
68 |
69 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
70 | # referring to absolute paths on developers' machines.
71 | system('rm -rf .symlinks')
72 | system('mkdir -p .symlinks/plugins')
73 | plugin_pods = parse_KV_file('../.flutter-plugins')
74 | plugin_pods.each do |name, path|
75 | symlink = File.join('.symlinks', 'plugins', name)
76 | File.symlink(path, symlink)
77 | pod name, :path => File.join(symlink, 'ios')
78 | end
79 | end
80 |
81 | # Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system.
82 | install! 'cocoapods', :disable_input_output_paths => true
83 |
84 | post_install do |installer|
85 | installer.pods_project.targets.each do |target|
86 | target.build_configurations.each do |config|
87 | config.build_settings['ENABLE_BITCODE'] = 'NO'
88 | end
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/lib/mailto.dart:
--------------------------------------------------------------------------------
1 | /// [Mailto] helps you create email ("mailto") links.
2 | ///
3 | /// The class takes care of all the necessary encoding.
4 | ///
5 | /// **Flutter info**
6 | ///
7 | /// You can use the `url_launcher` package for launching the
8 | /// URL that the [Mailto] class's [toString] method returns.
9 | /// It will open the default email client on the smart phone,
10 | /// with pre-filled [to], [cc], [bcc] recipient list, [subject], and [body]
11 | /// (content of the email).
12 | ///
13 | /// The user can still decide not to send an email or edit any of the
14 | /// fields.
15 | class Mailto {
16 | /// Create a [Mailto] instance.
17 | ///
18 | /// Convert the instance to `String` in order to get the `mailto` URL
19 | /// corresponding to the instance.
20 | ///
21 | /// Fields aren't validated. Check [Mailto.validateParameters] for more info.
22 | Mailto({
23 | this.to,
24 | this.cc,
25 | this.bcc,
26 | this.subject,
27 | this.body,
28 | });
29 |
30 | /// Validate the incoming parameters whether they would be valid for
31 | /// a mailto link.
32 | ///
33 | /// In case the parameters don't pass validation, [ArgumentError] is thrown.
34 | ///
35 | /// The [Mailto] class does not validate its fields neither at instatiation
36 | /// nor when the toString method is called, so make sure that you either
37 | /// know that the values are valid and the mailto links work on devices,
38 | /// or call the `validateParameters` function in an `assert` call to catch
39 | /// issues during development.
40 | static void validateParameters({
41 | List? to,
42 | List? cc,
43 | List? bcc,
44 | String? subject,
45 | String? body,
46 | }) {
47 | bool isEmptyString(String e) => e.isEmpty;
48 | bool containsLineBreak(String e) => e.contains('\n');
49 | if (to?.any(isEmptyString) == true) {
50 | throw ArgumentError.value(
51 | to,
52 | 'to',
53 | 'elements in "to" list must not be empty',
54 | );
55 | }
56 | if (to?.any(containsLineBreak) == true) {
57 | throw ArgumentError.value(
58 | to,
59 | 'to',
60 | 'elements in "to" list must not contain line breaks',
61 | );
62 | }
63 | if (cc?.any(isEmptyString) == true) {
64 | throw ArgumentError.value(
65 | cc,
66 | 'cc',
67 | 'elements in "cc" list must not be empty. ',
68 | );
69 | }
70 | if (cc?.any(containsLineBreak) == true) {
71 | throw ArgumentError.value(
72 | cc,
73 | 'cc',
74 | 'elements in "cc" list must not contain line breaks',
75 | );
76 | }
77 | if (bcc?.any(isEmptyString) == true) {
78 | throw ArgumentError.value(
79 | bcc,
80 | 'bcc',
81 | 'elements in "bcc" list must not be empty. ',
82 | );
83 | }
84 | if (bcc?.any(containsLineBreak) == true) {
85 | throw ArgumentError.value(
86 | bcc,
87 | 'bcc',
88 | 'elements in "bcc" list must not contain line breaks',
89 | );
90 | }
91 | if (subject?.contains('\n') == true) {
92 | throw ArgumentError.value(
93 | subject,
94 | 'subject',
95 | '"subject" must not contain line breaks',
96 | );
97 | }
98 | }
99 |
100 | /// Main recipient(s) of your email
101 | ///
102 | /// Destination email addresses.
103 | final List? to;
104 |
105 | /// Recipient(s) of a copy of the email.
106 | ///
107 | /// CC stands for carbon copy. When you CC people on an email, the CC list is
108 | /// visible to all other recipients.
109 | final List? cc;
110 |
111 | /// Recipient(s) of a secret copy of the email.
112 | ///
113 | /// BCC stands for blind carbon copy. When you BCC people on an email, the
114 | /// BCC list is not visible to other recipients.
115 | final List? bcc;
116 |
117 | /// Subject of email.
118 | final String? subject;
119 |
120 | /// Body of email.
121 | ///
122 | /// The content of the email.
123 | ///
124 | /// Please be aware that not all email clients are able to handle
125 | /// line-breaks in the body.
126 | final String? body;
127 |
128 | /// Percent encoded value of the comma (',') character.
129 | ///
130 | /// ```dart
131 | /// Uri.encodeComponent(',') == _comma; // true
132 | /// ```
133 | static const String _comma = '%2C';
134 |
135 | String _encodeTo(String s) {
136 | final atSign = s.lastIndexOf('@');
137 | return Uri.encodeComponent(s.substring(0, atSign)) + s.substring(atSign);
138 | }
139 |
140 | @override
141 | String toString() {
142 | // Use a string buffer as input is of unknown length.
143 | final stringBuffer = StringBuffer('mailto:');
144 | if (to != null) stringBuffer.writeAll(to!.map(_encodeTo), _comma);
145 | // We need this flag to know whether we should use & or ? when creating
146 | // the string.
147 | var parameterAdded = false;
148 | final parameterMap = {
149 | 'subject': subject,
150 | 'body': body,
151 | 'cc': cc?.join(','),
152 | 'bcc': bcc?.join(','),
153 | };
154 | for (final parameter in parameterMap.entries) {
155 | // Do not add key-value pair where the value is missing or empty
156 | if (parameter.value == null || parameter.value!.isEmpty) continue;
157 | // We don't need to encode the keys because all keys are under the
158 | // package's control currently and all of those keys are simple keys
159 | // without any special characters.
160 | // The values need to be encoded.
161 | // The RFC also mentions that the body should use '%0D%0A' for
162 | // line-breaks, however,we didn't find any difference between
163 | // '%0A' and '%0D%0A', so we keep it at '%0A'.
164 | stringBuffer
165 | ..write(parameterAdded ? '&' : '?')
166 | ..write(parameter.key)
167 | ..write('=')
168 | ..write(Uri.encodeComponent(parameter.value!));
169 | parameterAdded = true;
170 | }
171 | return stringBuffer.toString();
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `mailto`
2 |
3 | > Simple Dart package for creating mailto links in your Flutter and Dart apps
4 |
5 | The `mailto` package helps you build mailto links and provides you with an idiomatic Dart interface that:
6 |
7 | * supports one or many `to`, `cc`, and `bcc` fields
8 | * supports custom body and subject for the emails
9 | * encodes every value for your correctly
10 | * is blazingly fast ⚡️😜
11 |
12 | [](https://github.com/smaho-engineering)
13 |
14 | [](https://travis-ci.org/smaho-engineering/mailto 'Check build status on TravisCI') [](https://codecov.io/gh/smaho-engineering/mailto 'Check coverage info')
15 |
16 | [](https://pub.dev/packages/mailto 'See mailto package info on pub.dev')
17 | [](https://github.com/smaho-engineering/mailto 'Star our repository on GitHub!')
18 |
19 | ## Important links
20 |
21 | * [Read the source code and **star the repo!**](https://github.com/smaho-engineering/mailto)
22 | * [Check package info on `pub.dev`](https://pub.dev/packages/mailto)
23 | * [Open an issue](https://github.com/smaho-engineering/mailto/issues)
24 | * [Read the docs](https://pub.dev/documentation/mailto/latest/)
25 | * This Dart package is created by the [SMAHO development team](https://github.com/smaho-engineering)
26 |
27 | ## Usage
28 |
29 | You may want to launch the email client on your user's phone with certain fields pre-filled.
30 | For Flutter apps, it's recommended to use the [`url_launcher`](https://pub.dev/packages/url_launcher) package for launching the links you create with the `mailto` package.
31 |
32 | ```dart
33 | import 'package:mailto/mailto.dart';
34 | // For Flutter applications, you'll most likely want to use
35 | // the url_launcher package.
36 | import 'package:url_launcher/url_launcher.dart';
37 |
38 | // ...somewhere in your Flutter app...
39 | launchMailto() async {
40 | final mailtoLink = Mailto(
41 | to: ['to@example.com'],
42 | cc: ['cc1@example.com', 'cc2@example.com'],
43 | subject: 'mailto example subject',
44 | body: 'mailto example body',
45 | );
46 | // Convert the Mailto instance into a string.
47 | // Use either Dart's string interpolation
48 | // or the toString() method.
49 | await launch('$mailtoLink');
50 | }
51 | ```
52 |
53 | ### Validation
54 |
55 | The package provides a simple validation function.
56 | You could use this function in an `assert` to catch issues in development mode.
57 |
58 | The package doesn't validate automatically, so either use the validation function or make sure that the parameters you use are correct.
59 |
60 | ```dart
61 | Mailto.validateParameters(
62 | // New lines are NOT supported in subject lines
63 | subject: 'new lines in subject \n FTW',
64 | // What does this even mean?
65 | cc: ['\n\n\n', ''],
66 | );
67 | ```
68 |
69 | ## Known limitations of `mailto` URIs
70 |
71 | I tested the package manually in Flutter apps on iOS and Android (Gmail, FastMail, Yahoo email client), and in the browser on macOS,
72 | and the package has an extensive test suite that incorporates many examples from the [RFC 6068 - The 'mailto' URI Scheme](https://tools.ietf.org/html/rfc6068) document.
73 |
74 | Unfortunately, each client handle mailto links differently: Gmail does not add line-breaks in the message `body`,
75 | FastMail skips the `bcc`, Yahoo is not able to handle encoded values in `subject` and `body`, and these are only the three clients I tested.
76 | The iOS email client seems to handle everything well, so 🎸🤘🎉.
77 |
78 | The package might also not work if the resulting mailto links are extremely long. I don't know the exact character count where the links fail, but I'd try to keep things under 1000 characters.
79 |
80 | **Important**: Make sure you understand these limitations before you decide to incorporate `mailto` links in your app:
81 | letting users open their email clients with pre-filled values is a quick and easy way to let your users get in touch with you
82 | with extremely little development effort. At the same time, you need to keep in mind that it's very unlikely
83 | that these links are going to work consistently for all of your users.
84 |
85 | **If you need something bullet-proof, this package is not the right tool for solving your problem, so please consider alternative solutions (e.g. Flutter forms and a working backend).**
86 |
87 | In case you find potential improvements to the package, please create a pull request or let's discuss it in an issue.
88 | I might not merge all pull requests, especially changes that improve things for one client, but makes it worse for others.
89 | We consider the iOS mail app and Gmail on Android the two most important mail clients.
90 |
91 | ## Examples
92 |
93 | You'll find runnable, great examples on the project's GitHub repository in the [**`/example` folder**](https://github.com/smaho-engineering/mailto/tree/master/example).
94 |
95 | ### Flutter example app
96 |
97 | 1. Clone the repository
98 | 1. Change directory to `cd example/flutter`
99 | 1. `flutter run` and wait for the app to start
100 | 1. You can fill out the forms with your own input or click the "Surprise me" button to see how your mail client handles tricky input
101 |
102 | ### HTTP server serving an HTML web page with a mailto link
103 |
104 | The `mailto` package works in any Dart program: be it Flutter, AngularDart, or on the server.
105 |
106 | ```dart
107 | import 'dart:io';
108 |
109 | import 'package:mailto/mailto.dart';
110 |
111 | Future main() async {
112 | final mailto = Mailto(
113 | to: [
114 | 'example@example.com',
115 | 'ejemplo@ejemplo.com',
116 | ],
117 | cc: [
118 | 'percentage%100@example.com',
119 | 'QuestionMark?address@example.com',
120 | ],
121 | bcc: [
122 | 'Mike&family@example.org',
123 | ],
124 | subject: 'Let\'s drink a "café"! ☕️ 2+2=4 #coffeeAndMath',
125 | body:
126 | 'Hello this if the first line!\n\nNew line with some special characters őúóüűáéèßáñ\nEmoji: 🤪💙👍',
127 | );
128 |
129 | final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 3000);
130 | String renderHtml(Mailto mailto) => '''mailto exampleOpen mail client''';
131 | await for (HttpRequest request in server) {
132 | request.response
133 | ..statusCode = HttpStatus.ok
134 | ..headers.contentType = ContentType.html
135 | ..write(renderHtml(mailto));
136 | await request.response.close();
137 | }
138 | }
139 | ```
140 |
141 | 1. Clone the repository
142 | 1. Change directory to `cd example/http_server`
143 | 1. Start HTTP server `dart main.dart`
144 | 1. Open your browser and visit `localhost:3000`
145 | 1. Click on the link
146 | 1. If you have an email client installed on your computer, this client will be opened when you click the link on the HTML page.
147 |
148 | #### Screenshots
149 |
150 |
151 |
152 |
153 |
154 |
--------------------------------------------------------------------------------
/test/mailto_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:mailto/mailto.dart';
2 | import 'package:test/test.dart';
3 |
4 | void _test(String description, Mailto builder, String result) {
5 | test(description, () {
6 | expect('$builder', result);
7 | });
8 | }
9 |
10 | const _email = 'johndoe@example.com';
11 |
12 | void main() {
13 | group('$Mailto', () {
14 | group('validateInput', () {
15 | group('"to"', () {
16 | test('can be empty', () {
17 | expect(
18 | () => Mailto.validateParameters(to: []),
19 | returnsNormally,
20 | );
21 | });
22 |
23 | test('elements in "to" list must not be empty', () {
24 | expect(
25 | () => Mailto.validateParameters(to: ['']),
26 | throwsA(isA()),
27 | );
28 | });
29 |
30 | test('elements of the "to" list must not contain line breaks', () {
31 | expect(
32 | () => Mailto.validateParameters(to: ['new line\nshould throw']),
33 | throwsA(isA()),
34 | );
35 | });
36 |
37 | test('valid', () {
38 | expect(
39 | () => Mailto.validateParameters(to: ['example@example.com']),
40 | isNot(throwsA(isA())),
41 | );
42 | });
43 | });
44 |
45 | group('"cc"', () {
46 | const to = ['example@example.com'];
47 | test('may be empty list', () {
48 | expect(
49 | () => Mailto.validateParameters(to: to, cc: []),
50 | isNot(throwsA(isA())),
51 | );
52 | });
53 |
54 | test('elements in "cc" list must not be empty', () {
55 | expect(
56 | () => Mailto.validateParameters(
57 | to: to,
58 | cc: ['example2@example.com', ''],
59 | ),
60 | throwsA(isA()),
61 | );
62 | });
63 |
64 | test('elements of the "cc" list must not contain line breaks', () {
65 | expect(
66 | () => Mailto.validateParameters(
67 | to: to,
68 | cc: ['something\n@example.com'],
69 | ),
70 | throwsA(isA()),
71 | );
72 | });
73 |
74 | test('valid "cc"', () {
75 | expect(
76 | () => Mailto.validateParameters(
77 | to: to,
78 | cc: ['cc@example.com'],
79 | ),
80 | isNot(throwsA(isA())),
81 | );
82 | });
83 | });
84 |
85 | group('"bcc"', () {
86 | const to = ['example@example.com'];
87 | test('may be empty list', () {
88 | expect(
89 | () => Mailto.validateParameters(to: to, bcc: []),
90 | isNot(throwsA(isA())),
91 | );
92 | });
93 |
94 | test('elements in "bcc" list must not be empty', () {
95 | expect(
96 | () => Mailto.validateParameters(
97 | to: to,
98 | bcc: ['example2@example.com', ''],
99 | ),
100 | throwsA(isA()),
101 | );
102 | });
103 |
104 | test('elements of the "bcc" list must not contain line breaks', () {
105 | expect(
106 | () => Mailto.validateParameters(
107 | to: to,
108 | bcc: ['something\n@example.com'],
109 | ),
110 | throwsA(isA()),
111 | );
112 | });
113 |
114 | test('valid "bcc"', () {
115 | expect(
116 | () => Mailto.validateParameters(
117 | to: to,
118 | bcc: ['cc@example.com'],
119 | ),
120 | isNot(throwsA(isA())),
121 | );
122 | });
123 | });
124 |
125 | group('"subject"', () {
126 | const to = ['example@example.com'];
127 | test('must not contain line breaks', () {
128 | expect(
129 | () => Mailto.validateParameters(
130 | to: to,
131 | subject: 'Subject example\nwith new lines',
132 | ),
133 | throwsA(isA()),
134 | );
135 | });
136 |
137 | test('valid "subject"', () {
138 | expect(
139 | () => Mailto.validateParameters(
140 | to: to,
141 | subject: 'Valid subject',
142 | ),
143 | isNot(throwsA(isA())),
144 | );
145 | });
146 | });
147 |
148 | group('"body"', () {
149 | const to = ['example@example.com'];
150 | test('may be null', () {
151 | expect(
152 | () => Mailto.validateParameters(to: to, body: null),
153 | isNot(throwsA(isA())),
154 | );
155 | });
156 |
157 | test('may contain line breaks', () {
158 | expect(
159 | () => Mailto.validateParameters(
160 | to: to,
161 | body: 'Body example\nwith new lines',
162 | ),
163 | isNot(throwsA(isA())),
164 | );
165 | });
166 | });
167 | });
168 |
169 | group('"to"', () {
170 | void _testTo(String description, List? to, String result) {
171 | _test(description, Mailto(to: to), result);
172 | }
173 |
174 | _testTo(
175 | 'contains one simple email',
176 | ['example@example.com'],
177 | 'mailto:example@example.com',
178 | );
179 |
180 | _testTo(
181 | 'contains two simple emails',
182 | ['example@example.com', 'ejemplo@ejemplo.es'],
183 | 'mailto:example@example.com%2Cejemplo@ejemplo.es',
184 | );
185 |
186 | // https://tools.ietf.org/html/rfc6068#section-6.1
187 | _testTo(
188 | 'address contains percentage (%) sign',
189 | ['gorby%kremvax@example.com'],
190 | 'mailto:gorby%25kremvax@example.com',
191 | );
192 |
193 | // https://tools.ietf.org/html/rfc6068#section-6.1
194 | _testTo(
195 | 'address contains question mark (?) sign',
196 | ['unlikely?address@example.com'],
197 | 'mailto:unlikely%3Faddress@example.com',
198 | );
199 |
200 | // https://tools.ietf.org/html/rfc6068#section-6.1
201 | _testTo(
202 | 'address contains ampersand (&) sign',
203 | ['Mike&family@example.org'],
204 | 'mailto:Mike%26family@example.org',
205 | );
206 |
207 | // https://tools.ietf.org/html/rfc6068#section-6.2
208 | _testTo(
209 | 'complicated email address 1',
210 | ['"not@me"@example.com'],
211 | 'mailto:%22not%40me%22@example.com',
212 | );
213 |
214 | // https://tools.ietf.org/html/rfc6068#section-6.2
215 | _testTo(
216 | 'complicated email address 2',
217 | [r'"oh\\no"@example.org'],
218 | 'mailto:%22oh%5C%5Cno%22@example.org',
219 | );
220 |
221 | // https://tools.ietf.org/html/rfc6068#section-6.2
222 | _testTo(
223 | 'complicated email address 3',
224 | [r'''"\\\"it's\ ugly\\\""@example.org'''],
225 | '''mailto:%22%5C%5C%5C%22it's%5C%20ugly%5C%5C%5C%22%22@example.org''',
226 | );
227 |
228 | _testTo(
229 | 'null safety',
230 | null,
231 | 'mailto:',
232 | );
233 | });
234 |
235 | group('missing "to"', () {
236 | _test(
237 | 'to can be null',
238 | Mailto(subject: 'Subject'),
239 | 'mailto:?subject=Subject',
240 | );
241 |
242 | _test(
243 | 'to can be empty list',
244 | Mailto(to: [], subject: 'Subject'),
245 | 'mailto:?subject=Subject',
246 | );
247 | });
248 |
249 | group('"subject"', () {
250 | const email = 'subject@example.com';
251 | const to = [email];
252 |
253 | void _testSubject(String description, String subject, String result) {
254 | _test(description, Mailto(to: to, subject: subject), result);
255 | }
256 |
257 | _testSubject(
258 | 'is one word',
259 | 'Word',
260 | 'mailto:subject@example.com?subject=Word',
261 | );
262 |
263 | _testSubject(
264 | 'is multiple words',
265 | 'Multiple words subject',
266 | 'mailto:subject@example.com?subject=Multiple%20words%20subject',
267 | );
268 |
269 | _testSubject(
270 | 'contains & ampersand',
271 | 'Ampers & Sands Co.',
272 | 'mailto:subject@example.com?subject=Ampers%20%26%20Sands%20Co.',
273 | );
274 |
275 | _testSubject(
276 | 'contains + plus',
277 | '2+2',
278 | 'mailto:subject@example.com?subject=2%2B2',
279 | );
280 |
281 | _testSubject(
282 | 'contains = equals sign',
283 | '=3',
284 | 'mailto:subject@example.com?subject=%3D3',
285 | );
286 | });
287 | });
288 |
289 | group(
290 | '$Mailto legacy tests',
291 | () {
292 | _test(
293 | 'Only "to" address is set',
294 | Mailto(to: [_email]),
295 | 'mailto:$_email',
296 | );
297 |
298 | _test(
299 | 'Multiple to addresses',
300 | Mailto(
301 | to: [_email, 'vince@example.com'],
302 | subject: 'Hello Multiple',
303 | body: 'Mutliple to addresses',
304 | ),
305 | 'mailto:$_email%2Cvince@example.com?subject=Hello%20Multiple&body=Mutliple%20to%20addresses',
306 | );
307 |
308 | _test(
309 | 'Spaces, commas, and new lines in the "body"',
310 | Mailto(
311 | to: [_email],
312 | subject: 'Lorem Ipsum',
313 | body:
314 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
315 | ),
316 | 'mailto:$_email?subject=Lorem%20Ipsum&body=Lorem%20ipsum%20dolor%20sit%20amet%2C%20consectetur%20adipiscing%20elit%2C%20sed%20do%20eiusmod%20tempor%20incididunt%20ut%20labore%20et%20dolore%20magna%20aliqua.%0AUt%20enim%20ad%20minim%20veniam%2C%20quis%20nostrud%20exercitation%20ullamco%20laboris%20nisi%20ut%20aliquip%20ex%20ea%20commodo%20consequat.',
317 | );
318 |
319 | _test(
320 | 'One address in "cc"',
321 | Mailto(
322 | to: [_email],
323 | subject: 'Multiple CCs',
324 | body: 'Multiple CC body',
325 | cc: [
326 | 'vince@example.com',
327 | ],
328 | ),
329 | 'mailto:$_email?subject=Multiple%20CCs&body=Multiple%20CC%20body&cc=vince%40example.com',
330 | );
331 |
332 | _test(
333 | 'Multiple "cc" addresses are joined as encoded commas',
334 | Mailto(
335 | to: [_email],
336 | subject: 'Multiple CCs',
337 | body: 'Multiple CC body',
338 | cc: [
339 | 'vince@example.com',
340 | 'vicente@ejemplo.es',
341 | ],
342 | ),
343 | 'mailto:$_email?subject=Multiple%20CCs&body=Multiple%20CC%20body&cc=vince%40example.com%2Cvicente%40ejemplo.es',
344 | );
345 |
346 | _test(
347 | 'Empty "cc" list is skipped',
348 | Mailto(
349 | to: [_email],
350 | subject: 'EmptyCC',
351 | body: 'EmptyCCBody',
352 | cc: [],
353 | ),
354 | 'mailto:$_email?subject=EmptyCC&body=EmptyCCBody',
355 | );
356 |
357 | _test(
358 | 'Everything is set',
359 | Mailto(
360 | to: [_email],
361 | subject: 'cR@zY 3#4mPL3 with empty b0dy',
362 | body: '',
363 | cc: [
364 | 'vince@example.com',
365 | 'vicente@ejemplo.es',
366 | ],
367 | bcc: [
368 | 'secret@example.com',
369 | ],
370 | ),
371 | 'mailto:$_email?subject=cR%40zY%203%234mPL3%20with%20empty%20b0dy&cc=vince%40example.com%2Cvicente%40ejemplo.es&bcc=secret%40example.com',
372 | );
373 | },
374 | );
375 | }
376 |
--------------------------------------------------------------------------------
/example/flutter/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:mailto/mailto.dart';
3 | import 'package:url_launcher/url_launcher.dart';
4 |
5 | void main() => runApp(MailtoExampleApp());
6 |
7 | class MailtoExampleApp extends StatelessWidget {
8 | @override
9 | Widget build(BuildContext context) {
10 | return CupertinoApp(
11 | debugShowCheckedModeBanner: false,
12 | theme: CupertinoThemeData(
13 | textTheme: CupertinoTextThemeData(
14 | navLargeTitleTextStyle: TextStyle(
15 | fontWeight: FontWeight.bold,
16 | fontSize: 22.0,
17 | color: CupertinoColors.activeGreen,
18 | ),
19 | ),
20 | ),
21 | home: MailtoDemo(),
22 | );
23 | }
24 | }
25 |
26 | const Color white = Color(0xFFFFFFFF);
27 | const EdgeInsets textFieldPadding = EdgeInsets.all(16);
28 | const EdgeInsets iconPadding = EdgeInsets.fromLTRB(12, 0, 12, 0);
29 |
30 | class MailtoDemo extends StatefulWidget {
31 | @override
32 | _MailtoDemoState createState() => _MailtoDemoState();
33 | }
34 |
35 | class _MailtoDemoState extends State {
36 | List to = [];
37 | List cc = [];
38 | List bcc = [];
39 | String subject = '';
40 | String body = '';
41 |
42 | @override
43 | Widget build(BuildContext context) {
44 | return CupertinoPageScaffold(
45 | navigationBar: CupertinoNavigationBar(
46 | backgroundColor: white,
47 | middle: Text(
48 | 'mailto',
49 | ),
50 | ),
51 | child: ListView(
52 | children: [
53 | Padding(
54 | padding: const EdgeInsets.symmetric(
55 | horizontal: 12,
56 | ),
57 | child: CupertinoButton(
58 | color: Color(0xFF8E44AD),
59 | child: Text('Suprise me!'),
60 | onPressed: () async {
61 | final url = Mailto(
62 | to: [
63 | 'example@example.com',
64 | 'ejemplo@ejemplo.com',
65 | ],
66 | cc: [
67 | 'percentage%100@example.com',
68 | 'QuestionMark?address@example.com',
69 | ],
70 | bcc: [
71 | 'Mike&family@example.org',
72 | ],
73 | subject: 'Let\'s drink a "café"! ☕️ 2+2=4 #coffeeAndMath',
74 | body:
75 | 'Hello this if the first line!\n\nNew line with some special characters őúóüűáéèßáñ\nEmoji: 🤪💙👍',
76 | ).toString();
77 | if (await canLaunch(url)) {
78 | await launch(url);
79 | } else {
80 | showCupertinoDialog(
81 | context: context,
82 | builder: MailClientOpenErrorDialog(url: url).build,
83 | );
84 | }
85 | },
86 | ),
87 | ),
88 | EmailsContainer(
89 | onChanged: (v) => to = v,
90 | icon: CupertinoIcons.person_solid,
91 | title: 'to',
92 | placeholder: 'Recipient',
93 | ),
94 | EmailsContainer(
95 | onChanged: (v) => cc = v,
96 | icon: CupertinoIcons.group_solid,
97 | title: 'cc',
98 | placeholder: 'Carbon-copy',
99 | ),
100 | EmailsContainer(
101 | onChanged: (v) => bcc = v,
102 | icon: CupertinoIcons.group,
103 | title: 'bcc',
104 | placeholder: 'Blind Carbon-copy',
105 | ),
106 | SubjectTextField(
107 | onChanged: (v) => subject = v,
108 | ),
109 | BodyTextField(
110 | onChanged: (v) => body = v,
111 | ),
112 | Padding(
113 | padding: const EdgeInsets.symmetric(
114 | vertical: 32,
115 | horizontal: 12,
116 | ),
117 | child: CupertinoButton.filled(
118 | child: Text('Open Mail Client'),
119 | onPressed: () async {
120 | final url = Mailto(
121 | to: to,
122 | cc: cc,
123 | bcc: bcc,
124 | subject: subject,
125 | body: body,
126 | ).toString();
127 | if (await canLaunch(url)) {
128 | await launch(url);
129 | } else {
130 | showCupertinoDialog(
131 | context: context,
132 | builder: MailClientOpenErrorDialog(url: url).build,
133 | );
134 | }
135 | },
136 | ),
137 | ),
138 | ],
139 | ),
140 | );
141 | }
142 | }
143 |
144 | class MailClientOpenErrorDialog extends StatelessWidget {
145 | final String url;
146 |
147 | const MailClientOpenErrorDialog({Key? key, required this.url})
148 | : assert(url != ''),
149 | super(key: key);
150 |
151 | @override
152 | Widget build(BuildContext context) {
153 | return CupertinoAlertDialog(
154 | title: Text('Launch Error'),
155 | content: Text('We could not launch the following url:\n$url'),
156 | actions: [
157 | CupertinoDialogAction(
158 | isDefaultAction: true,
159 | child: Text('OK'),
160 | onPressed: () {
161 | Navigator.of(context).pop();
162 | },
163 | ),
164 | ],
165 | );
166 | }
167 | }
168 |
169 | class BodyTextField extends StatefulWidget {
170 | final ValueChanged onChanged;
171 |
172 | const BodyTextField({
173 | Key? key,
174 | required this.onChanged,
175 | }) : super(key: key);
176 |
177 | @override
178 | _BodyTextFieldState createState() => _BodyTextFieldState();
179 | }
180 |
181 | class _BodyTextFieldState extends State {
182 | final TextEditingController _controller = TextEditingController();
183 |
184 | bool isEnabled = false;
185 |
186 | @override
187 | void dispose() {
188 | _controller.dispose();
189 | super.dispose();
190 | }
191 |
192 | @override
193 | Widget build(BuildContext context) {
194 | return Column(
195 | mainAxisSize: MainAxisSize.min,
196 | children: [
197 | SectionHeading(
198 | leadingIcon: CupertinoIcons.book,
199 | title: 'body',
200 | trailingIcon:
201 | isEnabled ? CupertinoIcons.delete : CupertinoIcons.add_circled,
202 | onPressed: () {
203 | setState(() => isEnabled = !isEnabled);
204 | _controller.text = '';
205 | if (isEnabled) {
206 | widget.onChanged(_controller.text);
207 | }
208 | },
209 | ),
210 | if (isEnabled)
211 | Padding(
212 | padding: const EdgeInsets.fromLTRB(12, 4, 12, 4),
213 | child: CupertinoTextField(
214 | controller: _controller,
215 | padding: textFieldPadding,
216 | minLines: 4,
217 | maxLines: null,
218 | textCapitalization: TextCapitalization.sentences,
219 | keyboardType: TextInputType.multiline,
220 | placeholder: 'Body of the email',
221 | onChanged: widget.onChanged,
222 | ),
223 | ),
224 | ],
225 | );
226 | }
227 | }
228 |
229 | class SectionHeading extends StatelessWidget {
230 | final String title;
231 | final VoidCallback onPressed;
232 | final IconData leadingIcon;
233 | final IconData trailingIcon;
234 |
235 | const SectionHeading({
236 | Key? key,
237 | required this.title,
238 | required this.onPressed,
239 | required this.leadingIcon,
240 | required this.trailingIcon,
241 | }) : super(key: key);
242 |
243 | @override
244 | Widget build(BuildContext context) {
245 | return Flexible(
246 | child: Row(
247 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
248 | children: [
249 | Row(
250 | children: [
251 | Padding(
252 | padding: iconPadding,
253 | child: Icon(leadingIcon),
254 | ),
255 | LargeText(title),
256 | ],
257 | ),
258 | Padding(
259 | padding: const EdgeInsets.only(right: 4),
260 | child: CupertinoButton(
261 | child: Icon(trailingIcon),
262 | onPressed: onPressed,
263 | ),
264 | ),
265 | ],
266 | ),
267 | );
268 | }
269 | }
270 |
271 | class SubjectTextField extends StatefulWidget {
272 | final ValueChanged onChanged;
273 |
274 | const SubjectTextField({
275 | Key? key,
276 | required this.onChanged,
277 | }) : super(key: key);
278 |
279 | @override
280 | _SubjectTextFieldState createState() => _SubjectTextFieldState();
281 | }
282 |
283 | class _SubjectTextFieldState extends State {
284 | final TextEditingController _controller = TextEditingController();
285 |
286 | bool isEnabled = false;
287 |
288 | @override
289 | void dispose() {
290 | _controller.dispose();
291 | super.dispose();
292 | }
293 |
294 | @override
295 | Widget build(BuildContext context) {
296 | return Column(
297 | mainAxisSize: MainAxisSize.min,
298 | children: [
299 | SectionHeading(
300 | leadingIcon: CupertinoIcons.mail,
301 | title: 'subject',
302 | trailingIcon:
303 | isEnabled ? CupertinoIcons.delete : CupertinoIcons.add_circled,
304 | onPressed: () {
305 | setState(() => isEnabled = !isEnabled);
306 | _controller.text = '';
307 | if (isEnabled) {
308 | widget.onChanged(_controller.text);
309 | }
310 | },
311 | ),
312 | if (isEnabled)
313 | Padding(
314 | padding: const EdgeInsets.fromLTRB(12, 4, 12, 4),
315 | child: CupertinoTextField(
316 | controller: _controller,
317 | padding: textFieldPadding,
318 | textCapitalization: TextCapitalization.words,
319 | placeholder: 'Subject of the email',
320 | onChanged: widget.onChanged,
321 | ),
322 | ),
323 | ],
324 | );
325 | }
326 | }
327 |
328 | class LargeText extends StatelessWidget {
329 | final String data;
330 |
331 | const LargeText(this.data, {Key? key}) : super(key: key);
332 |
333 | @override
334 | Widget build(BuildContext context) {
335 | return Padding(
336 | padding: const EdgeInsets.symmetric(vertical: 16),
337 | child: Text(
338 | data,
339 | style: CupertinoTheme.of(context)
340 | .textTheme
341 | .navLargeTitleTextStyle
342 | .copyWith(color: CupertinoColors.black),
343 | ),
344 | );
345 | }
346 | }
347 |
348 | class EmailsContainer extends StatefulWidget {
349 | const EmailsContainer({
350 | Key? key,
351 | required this.title,
352 | required this.icon,
353 | required this.onChanged,
354 | required this.placeholder,
355 | }) : super(key: key);
356 |
357 | final String title;
358 | final String placeholder;
359 | final IconData icon;
360 | final ValueChanged> onChanged;
361 |
362 | @override
363 | _EmailsContainerState createState() => _EmailsContainerState();
364 | }
365 |
366 | class _EmailsContainerState extends State {
367 | final List _controllers = [];
368 |
369 | @override
370 | void dispose() {
371 | _controllers.forEach((c) => c.dispose());
372 | super.dispose();
373 | }
374 |
375 | @override
376 | Widget build(BuildContext context) {
377 | return Column(
378 | mainAxisSize: MainAxisSize.min,
379 | children: [
380 | SectionHeading(
381 | leadingIcon: widget.icon,
382 | trailingIcon: CupertinoIcons.plus_circled,
383 | title: widget.title,
384 | onPressed: () {
385 | setState(() {
386 | _controllers.add(TextEditingController(text: ''));
387 | });
388 | callOnChanged();
389 | },
390 | ),
391 | ...buildTextFields(),
392 | ],
393 | );
394 | }
395 |
396 | void callOnChanged() {
397 | widget.onChanged(_controllers.map((c) => c.text.trim()).toList());
398 | }
399 |
400 | void removeController(int index) {
401 | assert(index >= 0);
402 | assert(index < _controllers.length);
403 | setState(() => _controllers.removeAt(index));
404 | callOnChanged();
405 | }
406 |
407 | List buildTextFields() {
408 | final widgets = [];
409 | _controllers.asMap().forEach((index, controller) {
410 | widgets.add(Padding(
411 | padding: const EdgeInsets.fromLTRB(12, 4, 12, 4),
412 | child: CupertinoTextField(
413 | keyboardType: TextInputType.emailAddress,
414 | controller: controller,
415 | padding: textFieldPadding,
416 | suffix: CupertinoButton(
417 | padding: EdgeInsets.zero,
418 | onPressed: () => removeController(index),
419 | child: Padding(
420 | padding: const EdgeInsets.fromLTRB(16, 8, 8, 8),
421 | child: Icon(CupertinoIcons.minus_circled),
422 | ),
423 | ),
424 | textCapitalization: TextCapitalization.none,
425 | placeholder: '${widget.placeholder} ${index + 1}',
426 | onChanged: (_) => callOnChanged(),
427 | ),
428 | ));
429 | });
430 | return widgets;
431 | }
432 | }
433 |
--------------------------------------------------------------------------------
/example/flutter/ios/Runner.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
12 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
13 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
14 | 4CA115984980F1831515CAEA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E549DDF821F139FA1962ED84 /* Pods_Runner.framework */; };
15 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
16 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
17 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
18 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
19 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
20 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
21 | /* End PBXBuildFile section */
22 |
23 | /* Begin PBXCopyFilesBuildPhase section */
24 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
25 | isa = PBXCopyFilesBuildPhase;
26 | buildActionMask = 2147483647;
27 | dstPath = "";
28 | dstSubfolderSpec = 10;
29 | files = (
30 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
31 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
32 | );
33 | name = "Embed Frameworks";
34 | runOnlyForDeploymentPostprocessing = 0;
35 | };
36 | /* End PBXCopyFilesBuildPhase section */
37 |
38 | /* Begin PBXFileReference section */
39 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
40 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
41 | 3A355644FF4C98F60ED24FC4 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
42 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
43 | 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; };
44 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
45 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
46 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
47 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
48 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
49 | 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; };
50 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
51 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
52 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
53 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
54 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
55 | 997AEA74ACAF1D5BFD863884 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
56 | ABF66EEEF39A9C8F4FEA5962 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
57 | E549DDF821F139FA1962ED84 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
58 | /* End PBXFileReference section */
59 |
60 | /* Begin PBXFrameworksBuildPhase section */
61 | 97C146EB1CF9000F007C117D /* Frameworks */ = {
62 | isa = PBXFrameworksBuildPhase;
63 | buildActionMask = 2147483647;
64 | files = (
65 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
66 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
67 | 4CA115984980F1831515CAEA /* Pods_Runner.framework in Frameworks */,
68 | );
69 | runOnlyForDeploymentPostprocessing = 0;
70 | };
71 | /* End PBXFrameworksBuildPhase section */
72 |
73 | /* Begin PBXGroup section */
74 | 5E8F23BD9A964756E0D3E6DE /* Frameworks */ = {
75 | isa = PBXGroup;
76 | children = (
77 | E549DDF821F139FA1962ED84 /* Pods_Runner.framework */,
78 | );
79 | name = Frameworks;
80 | sourceTree = "";
81 | };
82 | 64122CDE2D74A7B646D84C1F /* Pods */ = {
83 | isa = PBXGroup;
84 | children = (
85 | 3A355644FF4C98F60ED24FC4 /* Pods-Runner.debug.xcconfig */,
86 | ABF66EEEF39A9C8F4FEA5962 /* Pods-Runner.release.xcconfig */,
87 | 997AEA74ACAF1D5BFD863884 /* Pods-Runner.profile.xcconfig */,
88 | );
89 | name = Pods;
90 | path = Pods;
91 | sourceTree = "";
92 | };
93 | 9740EEB11CF90186004384FC /* Flutter */ = {
94 | isa = PBXGroup;
95 | children = (
96 | 3B80C3931E831B6300D905FE /* App.framework */,
97 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
98 | 9740EEBA1CF902C7004384FC /* Flutter.framework */,
99 | 9740EEB21CF90195004384FC /* Debug.xcconfig */,
100 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
101 | 9740EEB31CF90195004384FC /* Generated.xcconfig */,
102 | );
103 | name = Flutter;
104 | sourceTree = "";
105 | };
106 | 97C146E51CF9000F007C117D = {
107 | isa = PBXGroup;
108 | children = (
109 | 9740EEB11CF90186004384FC /* Flutter */,
110 | 97C146F01CF9000F007C117D /* Runner */,
111 | 97C146EF1CF9000F007C117D /* Products */,
112 | 64122CDE2D74A7B646D84C1F /* Pods */,
113 | 5E8F23BD9A964756E0D3E6DE /* Frameworks */,
114 | );
115 | sourceTree = "";
116 | };
117 | 97C146EF1CF9000F007C117D /* Products */ = {
118 | isa = PBXGroup;
119 | children = (
120 | 97C146EE1CF9000F007C117D /* Runner.app */,
121 | );
122 | name = Products;
123 | sourceTree = "";
124 | };
125 | 97C146F01CF9000F007C117D /* Runner */ = {
126 | isa = PBXGroup;
127 | children = (
128 | 97C146FA1CF9000F007C117D /* Main.storyboard */,
129 | 97C146FD1CF9000F007C117D /* Assets.xcassets */,
130 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
131 | 97C147021CF9000F007C117D /* Info.plist */,
132 | 97C146F11CF9000F007C117D /* Supporting Files */,
133 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
134 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
135 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
136 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
137 | );
138 | path = Runner;
139 | sourceTree = "";
140 | };
141 | 97C146F11CF9000F007C117D /* Supporting Files */ = {
142 | isa = PBXGroup;
143 | children = (
144 | );
145 | name = "Supporting Files";
146 | sourceTree = "";
147 | };
148 | /* End PBXGroup section */
149 |
150 | /* Begin PBXNativeTarget section */
151 | 97C146ED1CF9000F007C117D /* Runner */ = {
152 | isa = PBXNativeTarget;
153 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
154 | buildPhases = (
155 | 1687C6D088A4C2786F3242EC /* [CP] Check Pods Manifest.lock */,
156 | 9740EEB61CF901F6004384FC /* Run Script */,
157 | 97C146EA1CF9000F007C117D /* Sources */,
158 | 97C146EB1CF9000F007C117D /* Frameworks */,
159 | 97C146EC1CF9000F007C117D /* Resources */,
160 | 9705A1C41CF9048500538489 /* Embed Frameworks */,
161 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
162 | 43EB83EAD49E6CEB0FB9B573 /* [CP] Embed Pods Frameworks */,
163 | );
164 | buildRules = (
165 | );
166 | dependencies = (
167 | );
168 | name = Runner;
169 | productName = Runner;
170 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
171 | productType = "com.apple.product-type.application";
172 | };
173 | /* End PBXNativeTarget section */
174 |
175 | /* Begin PBXProject section */
176 | 97C146E61CF9000F007C117D /* Project object */ = {
177 | isa = PBXProject;
178 | attributes = {
179 | LastUpgradeCheck = 1020;
180 | ORGANIZATIONNAME = "The Chromium Authors";
181 | TargetAttributes = {
182 | 97C146ED1CF9000F007C117D = {
183 | CreatedOnToolsVersion = 7.3.1;
184 | LastSwiftMigration = 1100;
185 | };
186 | };
187 | };
188 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
189 | compatibilityVersion = "Xcode 3.2";
190 | developmentRegion = en;
191 | hasScannedForEncodings = 0;
192 | knownRegions = (
193 | en,
194 | Base,
195 | );
196 | mainGroup = 97C146E51CF9000F007C117D;
197 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
198 | projectDirPath = "";
199 | projectRoot = "";
200 | targets = (
201 | 97C146ED1CF9000F007C117D /* Runner */,
202 | );
203 | };
204 | /* End PBXProject section */
205 |
206 | /* Begin PBXResourcesBuildPhase section */
207 | 97C146EC1CF9000F007C117D /* Resources */ = {
208 | isa = PBXResourcesBuildPhase;
209 | buildActionMask = 2147483647;
210 | files = (
211 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
212 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
213 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
214 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
215 | );
216 | runOnlyForDeploymentPostprocessing = 0;
217 | };
218 | /* End PBXResourcesBuildPhase section */
219 |
220 | /* Begin PBXShellScriptBuildPhase section */
221 | 1687C6D088A4C2786F3242EC /* [CP] Check Pods Manifest.lock */ = {
222 | isa = PBXShellScriptBuildPhase;
223 | buildActionMask = 2147483647;
224 | files = (
225 | );
226 | inputFileListPaths = (
227 | );
228 | inputPaths = (
229 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
230 | "${PODS_ROOT}/Manifest.lock",
231 | );
232 | name = "[CP] Check Pods Manifest.lock";
233 | outputFileListPaths = (
234 | );
235 | outputPaths = (
236 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
237 | );
238 | runOnlyForDeploymentPostprocessing = 0;
239 | shellPath = /bin/sh;
240 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
241 | showEnvVarsInLog = 0;
242 | };
243 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
244 | isa = PBXShellScriptBuildPhase;
245 | buildActionMask = 2147483647;
246 | files = (
247 | );
248 | inputPaths = (
249 | );
250 | name = "Thin Binary";
251 | outputPaths = (
252 | );
253 | runOnlyForDeploymentPostprocessing = 0;
254 | shellPath = /bin/sh;
255 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
256 | };
257 | 43EB83EAD49E6CEB0FB9B573 /* [CP] Embed Pods Frameworks */ = {
258 | isa = PBXShellScriptBuildPhase;
259 | buildActionMask = 2147483647;
260 | files = (
261 | );
262 | inputPaths = (
263 | );
264 | name = "[CP] Embed Pods Frameworks";
265 | outputPaths = (
266 | );
267 | runOnlyForDeploymentPostprocessing = 0;
268 | shellPath = /bin/sh;
269 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
270 | showEnvVarsInLog = 0;
271 | };
272 | 9740EEB61CF901F6004384FC /* Run Script */ = {
273 | isa = PBXShellScriptBuildPhase;
274 | buildActionMask = 2147483647;
275 | files = (
276 | );
277 | inputPaths = (
278 | );
279 | name = "Run Script";
280 | outputPaths = (
281 | );
282 | runOnlyForDeploymentPostprocessing = 0;
283 | shellPath = /bin/sh;
284 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
285 | };
286 | /* End PBXShellScriptBuildPhase section */
287 |
288 | /* Begin PBXSourcesBuildPhase section */
289 | 97C146EA1CF9000F007C117D /* Sources */ = {
290 | isa = PBXSourcesBuildPhase;
291 | buildActionMask = 2147483647;
292 | files = (
293 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
294 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
295 | );
296 | runOnlyForDeploymentPostprocessing = 0;
297 | };
298 | /* End PBXSourcesBuildPhase section */
299 |
300 | /* Begin PBXVariantGroup section */
301 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
302 | isa = PBXVariantGroup;
303 | children = (
304 | 97C146FB1CF9000F007C117D /* Base */,
305 | );
306 | name = Main.storyboard;
307 | sourceTree = "";
308 | };
309 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
310 | isa = PBXVariantGroup;
311 | children = (
312 | 97C147001CF9000F007C117D /* Base */,
313 | );
314 | name = LaunchScreen.storyboard;
315 | sourceTree = "";
316 | };
317 | /* End PBXVariantGroup section */
318 |
319 | /* Begin XCBuildConfiguration section */
320 | 249021D3217E4FDB00AE95B9 /* Profile */ = {
321 | isa = XCBuildConfiguration;
322 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
323 | buildSettings = {
324 | ALWAYS_SEARCH_USER_PATHS = NO;
325 | CLANG_ANALYZER_NONNULL = YES;
326 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
327 | CLANG_CXX_LIBRARY = "libc++";
328 | CLANG_ENABLE_MODULES = YES;
329 | CLANG_ENABLE_OBJC_ARC = YES;
330 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
331 | CLANG_WARN_BOOL_CONVERSION = YES;
332 | CLANG_WARN_COMMA = YES;
333 | CLANG_WARN_CONSTANT_CONVERSION = YES;
334 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
335 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
336 | CLANG_WARN_EMPTY_BODY = YES;
337 | CLANG_WARN_ENUM_CONVERSION = YES;
338 | CLANG_WARN_INFINITE_RECURSION = YES;
339 | CLANG_WARN_INT_CONVERSION = YES;
340 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
341 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
342 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
343 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
344 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
345 | CLANG_WARN_STRICT_PROTOTYPES = YES;
346 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
347 | CLANG_WARN_UNREACHABLE_CODE = YES;
348 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
349 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
350 | COPY_PHASE_STRIP = NO;
351 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
352 | ENABLE_NS_ASSERTIONS = NO;
353 | ENABLE_STRICT_OBJC_MSGSEND = YES;
354 | GCC_C_LANGUAGE_STANDARD = gnu99;
355 | GCC_NO_COMMON_BLOCKS = YES;
356 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
357 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
358 | GCC_WARN_UNDECLARED_SELECTOR = YES;
359 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
360 | GCC_WARN_UNUSED_FUNCTION = YES;
361 | GCC_WARN_UNUSED_VARIABLE = YES;
362 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
363 | MTL_ENABLE_DEBUG_INFO = NO;
364 | SDKROOT = iphoneos;
365 | SUPPORTED_PLATFORMS = iphoneos;
366 | TARGETED_DEVICE_FAMILY = "1,2";
367 | VALIDATE_PRODUCT = YES;
368 | };
369 | name = Profile;
370 | };
371 | 249021D4217E4FDB00AE95B9 /* Profile */ = {
372 | isa = XCBuildConfiguration;
373 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
374 | buildSettings = {
375 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
376 | CLANG_ENABLE_MODULES = YES;
377 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
378 | ENABLE_BITCODE = NO;
379 | FRAMEWORK_SEARCH_PATHS = (
380 | "$(inherited)",
381 | "$(PROJECT_DIR)/Flutter",
382 | );
383 | INFOPLIST_FILE = Runner/Info.plist;
384 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
385 | LIBRARY_SEARCH_PATHS = (
386 | "$(inherited)",
387 | "$(PROJECT_DIR)/Flutter",
388 | );
389 | PRODUCT_BUNDLE_IDENTIFIER = com.example.mailtoExample;
390 | PRODUCT_NAME = "$(TARGET_NAME)";
391 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
392 | SWIFT_VERSION = 5.0;
393 | VERSIONING_SYSTEM = "apple-generic";
394 | };
395 | name = Profile;
396 | };
397 | 97C147031CF9000F007C117D /* Debug */ = {
398 | isa = XCBuildConfiguration;
399 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
400 | buildSettings = {
401 | ALWAYS_SEARCH_USER_PATHS = NO;
402 | CLANG_ANALYZER_NONNULL = YES;
403 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
404 | CLANG_CXX_LIBRARY = "libc++";
405 | CLANG_ENABLE_MODULES = YES;
406 | CLANG_ENABLE_OBJC_ARC = YES;
407 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
408 | CLANG_WARN_BOOL_CONVERSION = YES;
409 | CLANG_WARN_COMMA = YES;
410 | CLANG_WARN_CONSTANT_CONVERSION = YES;
411 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
412 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
413 | CLANG_WARN_EMPTY_BODY = YES;
414 | CLANG_WARN_ENUM_CONVERSION = YES;
415 | CLANG_WARN_INFINITE_RECURSION = YES;
416 | CLANG_WARN_INT_CONVERSION = YES;
417 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
418 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
419 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
420 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
421 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
422 | CLANG_WARN_STRICT_PROTOTYPES = YES;
423 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
424 | CLANG_WARN_UNREACHABLE_CODE = YES;
425 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
426 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
427 | COPY_PHASE_STRIP = NO;
428 | DEBUG_INFORMATION_FORMAT = dwarf;
429 | ENABLE_STRICT_OBJC_MSGSEND = YES;
430 | ENABLE_TESTABILITY = YES;
431 | GCC_C_LANGUAGE_STANDARD = gnu99;
432 | GCC_DYNAMIC_NO_PIC = NO;
433 | GCC_NO_COMMON_BLOCKS = YES;
434 | GCC_OPTIMIZATION_LEVEL = 0;
435 | GCC_PREPROCESSOR_DEFINITIONS = (
436 | "DEBUG=1",
437 | "$(inherited)",
438 | );
439 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
440 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
441 | GCC_WARN_UNDECLARED_SELECTOR = YES;
442 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
443 | GCC_WARN_UNUSED_FUNCTION = YES;
444 | GCC_WARN_UNUSED_VARIABLE = YES;
445 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
446 | MTL_ENABLE_DEBUG_INFO = YES;
447 | ONLY_ACTIVE_ARCH = YES;
448 | SDKROOT = iphoneos;
449 | TARGETED_DEVICE_FAMILY = "1,2";
450 | };
451 | name = Debug;
452 | };
453 | 97C147041CF9000F007C117D /* Release */ = {
454 | isa = XCBuildConfiguration;
455 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
456 | buildSettings = {
457 | ALWAYS_SEARCH_USER_PATHS = NO;
458 | CLANG_ANALYZER_NONNULL = YES;
459 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
460 | CLANG_CXX_LIBRARY = "libc++";
461 | CLANG_ENABLE_MODULES = YES;
462 | CLANG_ENABLE_OBJC_ARC = YES;
463 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
464 | CLANG_WARN_BOOL_CONVERSION = YES;
465 | CLANG_WARN_COMMA = YES;
466 | CLANG_WARN_CONSTANT_CONVERSION = YES;
467 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
468 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
469 | CLANG_WARN_EMPTY_BODY = YES;
470 | CLANG_WARN_ENUM_CONVERSION = YES;
471 | CLANG_WARN_INFINITE_RECURSION = YES;
472 | CLANG_WARN_INT_CONVERSION = YES;
473 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
474 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
475 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
476 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
477 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
478 | CLANG_WARN_STRICT_PROTOTYPES = YES;
479 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
480 | CLANG_WARN_UNREACHABLE_CODE = YES;
481 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
482 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
483 | COPY_PHASE_STRIP = NO;
484 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
485 | ENABLE_NS_ASSERTIONS = NO;
486 | ENABLE_STRICT_OBJC_MSGSEND = YES;
487 | GCC_C_LANGUAGE_STANDARD = gnu99;
488 | GCC_NO_COMMON_BLOCKS = YES;
489 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
490 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
491 | GCC_WARN_UNDECLARED_SELECTOR = YES;
492 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
493 | GCC_WARN_UNUSED_FUNCTION = YES;
494 | GCC_WARN_UNUSED_VARIABLE = YES;
495 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
496 | MTL_ENABLE_DEBUG_INFO = NO;
497 | SDKROOT = iphoneos;
498 | SUPPORTED_PLATFORMS = iphoneos;
499 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
500 | TARGETED_DEVICE_FAMILY = "1,2";
501 | VALIDATE_PRODUCT = YES;
502 | };
503 | name = Release;
504 | };
505 | 97C147061CF9000F007C117D /* Debug */ = {
506 | isa = XCBuildConfiguration;
507 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
508 | buildSettings = {
509 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
510 | CLANG_ENABLE_MODULES = YES;
511 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
512 | ENABLE_BITCODE = NO;
513 | FRAMEWORK_SEARCH_PATHS = (
514 | "$(inherited)",
515 | "$(PROJECT_DIR)/Flutter",
516 | );
517 | INFOPLIST_FILE = Runner/Info.plist;
518 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
519 | LIBRARY_SEARCH_PATHS = (
520 | "$(inherited)",
521 | "$(PROJECT_DIR)/Flutter",
522 | );
523 | PRODUCT_BUNDLE_IDENTIFIER = com.example.mailtoExample;
524 | PRODUCT_NAME = "$(TARGET_NAME)";
525 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
526 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
527 | SWIFT_VERSION = 5.0;
528 | VERSIONING_SYSTEM = "apple-generic";
529 | };
530 | name = Debug;
531 | };
532 | 97C147071CF9000F007C117D /* Release */ = {
533 | isa = XCBuildConfiguration;
534 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
535 | buildSettings = {
536 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
537 | CLANG_ENABLE_MODULES = YES;
538 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
539 | ENABLE_BITCODE = NO;
540 | FRAMEWORK_SEARCH_PATHS = (
541 | "$(inherited)",
542 | "$(PROJECT_DIR)/Flutter",
543 | );
544 | INFOPLIST_FILE = Runner/Info.plist;
545 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
546 | LIBRARY_SEARCH_PATHS = (
547 | "$(inherited)",
548 | "$(PROJECT_DIR)/Flutter",
549 | );
550 | PRODUCT_BUNDLE_IDENTIFIER = com.example.mailtoExample;
551 | PRODUCT_NAME = "$(TARGET_NAME)";
552 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
553 | SWIFT_VERSION = 5.0;
554 | VERSIONING_SYSTEM = "apple-generic";
555 | };
556 | name = Release;
557 | };
558 | /* End XCBuildConfiguration section */
559 |
560 | /* Begin XCConfigurationList section */
561 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
562 | isa = XCConfigurationList;
563 | buildConfigurations = (
564 | 97C147031CF9000F007C117D /* Debug */,
565 | 97C147041CF9000F007C117D /* Release */,
566 | 249021D3217E4FDB00AE95B9 /* Profile */,
567 | );
568 | defaultConfigurationIsVisible = 0;
569 | defaultConfigurationName = Release;
570 | };
571 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
572 | isa = XCConfigurationList;
573 | buildConfigurations = (
574 | 97C147061CF9000F007C117D /* Debug */,
575 | 97C147071CF9000F007C117D /* Release */,
576 | 249021D4217E4FDB00AE95B9 /* Profile */,
577 | );
578 | defaultConfigurationIsVisible = 0;
579 | defaultConfigurationName = Release;
580 | };
581 | /* End XCConfigurationList section */
582 | };
583 | rootObject = 97C146E61CF9000F007C117D /* Project object */;
584 | }
585 |
--------------------------------------------------------------------------------